mirror of
https://github.com/home-assistant/core.git
synced 2026-06-26 16:45:29 +02:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6bbf2f78f | |||
| 7dd5e188bb | |||
| ec80260c4c | |||
| 10ceac63f6 | |||
| 847f4dc287 | |||
| 8d4c8114d4 | |||
| b6b165fd00 | |||
| 7c99cf6385 | |||
| d6b743b93e | |||
| 0cae5e41b4 | |||
| 6fce245dfa | |||
| 44ba231bf6 | |||
| 2be55a06cc | |||
| d786fb16a0 | |||
| f78dd797b1 | |||
| 0d957a971d | |||
| cff3a711f3 | |||
| 177c4a4fb5 | |||
| 7d8204f5e7 | |||
| 9aed167f71 | |||
| a8630f5570 | |||
| 2a75b0e2fb | |||
| 9c4ad761c4 | |||
| 8e3e1044a1 | |||
| bec6c94e32 | |||
| c9729df69a | |||
| 70ff0fd682 | |||
| 258ae6d506 | |||
| 4f93afd6ae | |||
| 7968fc4809 | |||
| 975f2a831e | |||
| cc2944d626 | |||
| 548ec5cacf | |||
| dc6eef2844 | |||
| 0808e30e37 | |||
| f0ed257f47 | |||
| b4b710b474 | |||
| 0004a82fe4 | |||
| 0c4bc95bdd | |||
| 5fdab795e8 | |||
| 2193665909 | |||
| c9d91d5812 | |||
| de9d9c66c1 | |||
| dfcc4d1ae4 | |||
| d71812f09b | |||
| a323ebe634 | |||
| 024bba55cf | |||
| a5546566e7 | |||
| 3d9994ee4f |
@@ -39,7 +39,7 @@ on:
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.7"
|
||||
HA_SHORT_VERSION: "2026.8"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
|
||||
Generated
+2
-3
@@ -230,7 +230,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/battery/ @home-assistant/core
|
||||
/homeassistant/components/bayesian/ @HarvsG
|
||||
/tests/components/bayesian/ @HarvsG
|
||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||
/homeassistant/components/binary_sensor/ @home-assistant/core
|
||||
/tests/components/binary_sensor/ @home-assistant/core
|
||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||
@@ -791,8 +790,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/html5/ @alexyao2015 @tr4nt0r
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
/tests/components/http/ @home-assistant/core
|
||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||
/tests/components/huawei_lte/ @scop @fphammerle
|
||||
/homeassistant/components/huawei_lte/ @fphammerle
|
||||
/tests/components/huawei_lte/ @fphammerle
|
||||
/homeassistant/components/hue/ @marcelveldt
|
||||
/tests/components/hue/ @marcelveldt
|
||||
/homeassistant/components/hue_ble/ @flip-dots
|
||||
|
||||
@@ -28,7 +28,7 @@ async def async_update_unique_id(
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{old_key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
DOMAIN, platform, unique_id
|
||||
platform, DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
||||
new_unique_id = unique_id.replace(old_key, new_key)
|
||||
@@ -48,7 +48,7 @@ async def async_remove_entity_from_virtual_group(
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
|
||||
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
@@ -70,7 +70,7 @@ async def async_remove_unsupported_notification_sensors(
|
||||
):
|
||||
unique_id = f"{serial_num}-{notification_key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SENSOR_DOMAIN, unique_id=unique_id
|
||||
SENSOR_DOMAIN, DOMAIN, unique_id=unique_id
|
||||
)
|
||||
is_unsupported = not coordinator.data[serial_num].notifications_supported
|
||||
|
||||
|
||||
@@ -731,17 +731,32 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
trace_element = TraceElement(variables, trigger_path)
|
||||
trace_append_element(trace_element)
|
||||
|
||||
if (
|
||||
not skip_condition
|
||||
and self._condition is not None
|
||||
and not self._condition.async_check(variables=variables)
|
||||
):
|
||||
self._logger.debug(
|
||||
"Conditions not met, aborting automation. Condition summary: %s",
|
||||
trace_get(clear=False),
|
||||
)
|
||||
script_execution_set("failed_conditions")
|
||||
return None
|
||||
if not skip_condition and self._condition is not None:
|
||||
try:
|
||||
conditions_pass = self._condition.async_check(variables=variables)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
self._logger.error(
|
||||
"Error while checking conditions of automation %s: %s",
|
||||
self.entity_id,
|
||||
err,
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
return None
|
||||
except Exception as err:
|
||||
self._logger.exception(
|
||||
"Unexpected error while checking conditions of automation %s",
|
||||
self.entity_id,
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
return None
|
||||
|
||||
if not conditions_pass:
|
||||
self._logger.debug(
|
||||
"Conditions not met, aborting automation. Condition summary: %s",
|
||||
trace_get(clear=False),
|
||||
)
|
||||
script_execution_set("failed_conditions")
|
||||
return None
|
||||
|
||||
self.async_set_context(trigger_context)
|
||||
event_data = {
|
||||
@@ -794,7 +809,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
except Exception as err:
|
||||
self._logger.exception("While executing automation %s", self.entity_id)
|
||||
self._logger.exception(
|
||||
"Unexpected error while executing automation %s", self.entity_id
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The avion component."""
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Support for Avion dimmers."""
|
||||
|
||||
import importlib
|
||||
import time
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_DEVICES,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ID): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Avion switch."""
|
||||
avion = importlib.import_module("avion")
|
||||
|
||||
lights = [
|
||||
AvionLight(
|
||||
avion.Avion(
|
||||
mac=address,
|
||||
passphrase=device_config[CONF_API_KEY],
|
||||
name=device_config.get(CONF_NAME),
|
||||
object_id=device_config.get(CONF_ID),
|
||||
connect=False,
|
||||
)
|
||||
)
|
||||
for address, device_config in config[CONF_DEVICES].items()
|
||||
]
|
||||
if CONF_USERNAME in config and CONF_PASSWORD in config:
|
||||
lights.extend(
|
||||
AvionLight(device)
|
||||
for device in avion.get_devices(
|
||||
config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
)
|
||||
)
|
||||
|
||||
add_entities(lights)
|
||||
|
||||
|
||||
class AvionLight(LightEntity):
|
||||
"""Representation of an Avion light."""
|
||||
|
||||
_attr_support_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_support_color_modes = {ColorMode.BRIGHTNESS}
|
||||
_attr_should_poll = False
|
||||
_attr_assumed_state = True
|
||||
_attr_is_on = True
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
self._attr_name = device.name
|
||||
self._attr_unique_id = device.mac
|
||||
self._attr_brightness = 255
|
||||
self._switch = device
|
||||
|
||||
def set_state(self, brightness):
|
||||
"""Set the state of this lamp to the provided brightness."""
|
||||
avion = importlib.import_module("avion")
|
||||
|
||||
# Bluetooth LE is unreliable, and the connection may drop at any
|
||||
# time. Make an effort to re-establish the link.
|
||||
initial = time.monotonic()
|
||||
while True:
|
||||
if time.monotonic() - initial >= 10:
|
||||
return False
|
||||
try:
|
||||
self._switch.set_brightness(brightness)
|
||||
break
|
||||
except avion.AvionException:
|
||||
self._switch.connect()
|
||||
return True
|
||||
|
||||
@override
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the specified or all lights on."""
|
||||
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
|
||||
self._attr_brightness = brightness
|
||||
|
||||
self.set_state(self.brightness)
|
||||
self._attr_is_on = True
|
||||
|
||||
@override
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the specified or all lights off."""
|
||||
self.set_state(0)
|
||||
self._attr_is_on = False
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "avion",
|
||||
"name": "Avi-on",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/avion",
|
||||
"iot_class": "assumed_state",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["avion==0.10"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
"""The beewi_smartclim component."""
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "beewi_smartclim",
|
||||
"name": "BeeWi SmartClim BLE sensor",
|
||||
"codeowners": ["@alemuro"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/beewi_smartclim",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["beewi_smartclim"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["beewi-smartclim==0.0.10"]
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Platform for beewi_smartclim integration."""
|
||||
|
||||
from beewi_smartclim import BeewiSmartClimPoller
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME, PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
# Default values
|
||||
DEFAULT_NAME = "BeeWi SmartClim"
|
||||
|
||||
# Sensor config
|
||||
SENSOR_TYPES = [
|
||||
[SensorDeviceClass.TEMPERATURE, "Temperature", UnitOfTemperature.CELSIUS],
|
||||
[SensorDeviceClass.HUMIDITY, "Humidity", PERCENTAGE],
|
||||
[SensorDeviceClass.BATTERY, "Battery", PERCENTAGE],
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the beewi_smartclim platform."""
|
||||
|
||||
mac = config[CONF_MAC]
|
||||
prefix = config[CONF_NAME]
|
||||
poller = BeewiSmartClimPoller(mac)
|
||||
|
||||
sensors = []
|
||||
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
device = sensor_type[0]
|
||||
name = sensor_type[1]
|
||||
unit = sensor_type[2]
|
||||
# `prefix` is the name configured by the user for the sensor, we're appending
|
||||
# the device type at the end of the name (garden -> garden temperature)
|
||||
if prefix:
|
||||
name = f"{prefix} {name}"
|
||||
|
||||
sensors.append(BeewiSmartclimSensor(poller, name, mac, device, unit))
|
||||
|
||||
add_entities(sensors)
|
||||
|
||||
|
||||
class BeewiSmartclimSensor(SensorEntity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
def __init__(self, poller, name, mac, device, unit):
|
||||
"""Initialize the sensor."""
|
||||
self._poller = poller
|
||||
self._attr_name = name
|
||||
self._device = device
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_device_class = self._device
|
||||
self._attr_unique_id = f"{mac}_{device}"
|
||||
|
||||
def update(self) -> None:
|
||||
"""Fetch new state data from the poller."""
|
||||
self._poller.update_sensor()
|
||||
self._attr_native_value = None
|
||||
if self._device == SensorDeviceClass.TEMPERATURE:
|
||||
self._attr_native_value = self._poller.get_temperature()
|
||||
if self._device == SensorDeviceClass.HUMIDITY:
|
||||
self._attr_native_value = self._poller.get_humidity()
|
||||
if self._device == SensorDeviceClass.BATTERY:
|
||||
self._attr_native_value = self._poller.get_battery()
|
||||
@@ -15,16 +15,15 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfApparentPower,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfRatio,
|
||||
UnitOfReactiveEnergy,
|
||||
UnitOfReactivePower,
|
||||
UnitOfSpeed,
|
||||
@@ -53,19 +52,19 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm1",
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm2_5",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
@@ -84,7 +83,7 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
@@ -179,7 +178,7 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.22",
|
||||
"habluetooth==6.23.1"
|
||||
"habluetooth==6.25.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -805,6 +805,10 @@ class DefaultAgent(ConversationEntity):
|
||||
else:
|
||||
num_unmatched_entities += 1
|
||||
|
||||
# Literal text matched is the dominant signal
|
||||
same_text_matched = (maybe_result is not None) and (
|
||||
result.text_chunks_matched == maybe_result.text_chunks_matched
|
||||
)
|
||||
if (
|
||||
(maybe_result is None) # first result
|
||||
or (
|
||||
@@ -813,22 +817,25 @@ class DefaultAgent(ConversationEntity):
|
||||
)
|
||||
or (
|
||||
# More entities matched
|
||||
num_matched_entities > best_num_matched_entities
|
||||
same_text_matched
|
||||
and (num_matched_entities > best_num_matched_entities)
|
||||
)
|
||||
or (
|
||||
# Fewer unmatched entities
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
same_text_matched
|
||||
and (num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities < best_num_unmatched_entities)
|
||||
)
|
||||
or (
|
||||
# Prefer unmatched ranges
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
same_text_matched
|
||||
and (num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||
)
|
||||
or (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
same_text_matched
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||
and (
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.8.0", "home-assistant-intents==2026.6.1"]
|
||||
"requirements": ["hassil==3.8.0", "home-assistant-intents==2026.6.24"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .auth import DropboxConfigEntryAuth
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
|
||||
|
||||
@@ -31,6 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> b
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
token = entry.data["token"]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_scopes",
|
||||
)
|
||||
if "refresh_token" not in token:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_refresh_token",
|
||||
)
|
||||
|
||||
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
|
||||
|
||||
auth = DropboxConfigEntryAuth(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Application credentials platform for the Dropbox integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -9,14 +7,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||
) -> AbstractOAuth2Implementation:
|
||||
"""Return custom auth implementation."""
|
||||
return DropboxOAuth2Implementation(
|
||||
"""Return auth implementation."""
|
||||
return LocalOAuth2ImplementationWithPkce(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
@@ -24,21 +22,3 @@ async def async_get_auth_implementation(
|
||||
OAUTH2_TOKEN,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Custom Dropbox OAuth2 implementation.
|
||||
|
||||
Adds the necessary authorize url parameters.
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data: dict = {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .auth import DropboxConfigFlowAuth
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
|
||||
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
@@ -26,6 +26,15 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
|
||||
@override
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
@@ -51,6 +60,9 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
token = entry_data[CONF_TOKEN]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
return await self.async_step_reauth_permissions()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
@@ -60,3 +72,11 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reauth_permissions(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that additional permissions are required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_permissions")
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -12,6 +12,7 @@ OAUTH2_SCOPES = [
|
||||
"account_info.read",
|
||||
"files.content.read",
|
||||
"files.content.write",
|
||||
"files.metadata.read",
|
||||
]
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dropbox",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-dropbox-api==0.1.4"]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ rules:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: Integration does not make any entity updates.
|
||||
|
||||
@@ -24,10 +24,20 @@
|
||||
"reauth_confirm": {
|
||||
"description": "The Dropbox integration needs to re-authenticate your account.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"reauth_permissions": {
|
||||
"description": "The Dropbox integration requires additional permissions to function correctly.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"missing_refresh_token": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_confirm::description%]"
|
||||
},
|
||||
"missing_scopes": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_permissions::description%]"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfRatio,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -72,7 +71,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="target_flow_level",
|
||||
translation_key="target_flow_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda node: (
|
||||
node.ventilation.flow_lvl_tgt if node.ventilation else None
|
||||
@@ -96,7 +95,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
value_fn=lambda node: node.sensor.co2 if node.sensor else None,
|
||||
node_types=(
|
||||
NodeType.BSCO2,
|
||||
@@ -108,7 +107,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_co2",
|
||||
translation_key="iaq_co2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None,
|
||||
@@ -123,14 +122,14 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_fn=lambda node: node.sensor.rh if node.sensor else None,
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_rh",
|
||||
translation_key="iaq_rh",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_rh if node.sensor else None,
|
||||
|
||||
@@ -641,25 +641,34 @@ class Thermostat(ClimateEntity):
|
||||
for device in device_registry.devices.values()
|
||||
for sensor_info in sensors_info
|
||||
if device.name == sensor_info["name"]
|
||||
and any(identifier[0] == DOMAIN for identifier in device.identifiers)
|
||||
]
|
||||
|
||||
def _active_climate_name(self) -> str:
|
||||
"""Return the ecobee climate name of the active comfort setting.
|
||||
|
||||
``preset_mode`` is the climate *name*, but ``_preset_modes`` is keyed by
|
||||
climateRef, so the built-in presets are translated back to their ecobee
|
||||
name. Holds that are not a comfort setting (temperature/vacation/
|
||||
indefinite away) are not real climates; per ecobee they follow the Home
|
||||
comfort setting's sensor participation, so fall back to "Home".
|
||||
"""
|
||||
# https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
|
||||
preset_mode = self.preset_mode
|
||||
if preset_mode is None:
|
||||
return "Home"
|
||||
mode = HASS_TO_ECOBEE_PRESET.get(preset_mode, preset_mode)
|
||||
return mode if mode in self._preset_modes.values() else "Home"
|
||||
|
||||
@property
|
||||
def active_sensors_in_preset_mode(self) -> list:
|
||||
"""Return the currently active/participating sensors."""
|
||||
# https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
|
||||
# During a manual hold, the ecobee will follow the Sensor Participation
|
||||
# rules for the Home Comfort Settings
|
||||
mode = self._preset_modes.get(self.preset_mode, "Home")
|
||||
return self._sensors_in_preset_mode(mode)
|
||||
return self._sensors_in_preset_mode(self._active_climate_name())
|
||||
|
||||
@property
|
||||
def active_sensor_devices_in_preset_mode(self) -> list:
|
||||
"""Return the currently active/participating sensor devices."""
|
||||
# https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
|
||||
# During a manual hold, the ecobee will follow the Sensor Participation
|
||||
# rules for the Home Comfort Settings
|
||||
mode = self._preset_modes.get(self.preset_mode, "Home")
|
||||
return self._sensor_devices_in_preset_mode(mode)
|
||||
return self._sensor_devices_in_preset_mode(self._active_climate_name())
|
||||
|
||||
@override
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
@@ -950,6 +959,7 @@ class Thermostat(ClimateEntity):
|
||||
for device in device_registry.devices.values()
|
||||
for sensor_name in sensor_names
|
||||
if device.name == sensor_name
|
||||
and any(identifier[0] == DOMAIN for identifier in device.identifiers)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"configure_voice": "Configure advanced voice settings",
|
||||
"configure_voice": "Configure voice settings",
|
||||
"model": "Model",
|
||||
"stt_auto_language": "Auto-detect language",
|
||||
"stt_model": "Speech-to-text model",
|
||||
"voice": "Voice"
|
||||
},
|
||||
"data_description": {
|
||||
"configure_voice": "Configure advanced voice settings. Find more information in the ElevenLabs documentation.",
|
||||
"configure_voice": "Configure voice settings. Find more information in the ElevenLabs documentation.",
|
||||
"model": "ElevenLabs model to use. Please note that not all models support all languages equally well.",
|
||||
"stt_auto_language": "Automatically detect the spoken language for speech-to-text.",
|
||||
"stt_model": "Speech-to-text model to use.",
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260527.7"]
|
||||
"requirements": ["home-assistant-frontend==20260624.0"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.const import EntityCategory, UnitOfRatio, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
@@ -67,7 +67,7 @@ BSH_PROGRAM_SENSORS = (
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
translation_key="program_progress",
|
||||
appliance_types=APPLIANCES_WITH_PROGRAMS,
|
||||
),
|
||||
@@ -158,6 +158,7 @@ SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE,
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
"data": {
|
||||
"channel": "Channel"
|
||||
},
|
||||
"description": "Start a channel change for your Zigbee and Thread networks.\n\nNote: this is an advanced operation and can leave your Thread and Zigbee networks inoperable if the new channel is congested. Depending on existing network conditions, many of your devices may not migrate to the new channel and will require re-joining before they start working again. Use with caution.\n\nOnce you have selected **Submit**, the channel change starts quietly in the background and will finish after a few minutes.",
|
||||
"description": "Start a channel change for your Zigbee and Thread networks.\n\nNote: this operation can leave your Thread and Zigbee networks inoperable if the new channel is congested. Depending on existing network conditions, many of your devices may not migrate to the new channel and will require re-joining before they start working again. Use with caution.\n\nOnce you have selected **Submit**, the channel change starts quietly in the background and will finish after a few minutes.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]"
|
||||
},
|
||||
"install_addon": {
|
||||
|
||||
@@ -26,18 +26,17 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
@@ -254,7 +253,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
name="Current Humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
# This sensor is only for humidity characteristics that are not part
|
||||
# of a humidity sensor service.
|
||||
probe=(lambda char: char.service.type != ServicesTypes.HUMIDITY_SENSOR),
|
||||
@@ -270,42 +269,42 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
name="PM2.5 Density",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_PM10: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_PM10,
|
||||
name="PM10 Density",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_OZONE: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_OZONE,
|
||||
name="Ozone Density",
|
||||
device_class=SensorDeviceClass.OZONE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_NO2: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_NO2,
|
||||
name="Nitrogen Dioxide Density",
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_SO2: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_SO2,
|
||||
name="Sulphur Dioxide Density",
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_VOC: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_VOC,
|
||||
name="Volatile Organic Compound Density",
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.THREAD_NODE_CAPABILITIES: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES,
|
||||
@@ -363,13 +362,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
key=CharacteristicsTypes.FILTER_LIFE_LEVEL,
|
||||
name="Filter lifetime",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
),
|
||||
CharacteristicsTypes.WATER_LEVEL: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.WATER_LEVEL,
|
||||
name="Water level",
|
||||
translation_key="water_level",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
CharacteristicsTypes.VENDOR_EVE_THERMO_VALVE_POSITION: (
|
||||
@@ -379,7 +378,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
translation_key="valve_position",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
)
|
||||
),
|
||||
}
|
||||
@@ -409,7 +408,7 @@ class HomeKitHumiditySensor(HomeKitSensor):
|
||||
"""Representation of a Homekit humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
|
||||
@override
|
||||
def get_characteristic_types(self) -> list[str]:
|
||||
@@ -481,7 +480,7 @@ class HomeKitCarbonDioxideSensor(HomeKitSensor):
|
||||
"""Representation of a Homekit Carbon Dioxide sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.CO2
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PARTS_PER_MILLION
|
||||
|
||||
@override
|
||||
def get_characteristic_types(self) -> list[str]:
|
||||
@@ -505,7 +504,7 @@ class HomeKitBatterySensor(HomeKitSensor):
|
||||
"""Representation of a Homekit battery sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
@override
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.13.1"]
|
||||
"requirements": ["homematicip==2.13.2"]
|
||||
}
|
||||
|
||||
@@ -50,14 +50,13 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfDensity,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfRatio,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
@@ -84,7 +83,7 @@ SMOKE_DETECTOR_SENSORS: tuple[HmipSmokeDetectorSensorDescription, ...] = (
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="dirt_level",
|
||||
translation_key="smoke_detector_dirt_level",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="dirtLevel",
|
||||
@@ -532,7 +531,7 @@ class HomematicipFloorTerminalBlockMechanicChannelValve(
|
||||
):
|
||||
"""Representation of the HomematicIP floor terminal block."""
|
||||
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
@@ -581,7 +580,7 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of then HomeMaticIP access point."""
|
||||
|
||||
_attr_icon = "mdi:access-point-network"
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -600,7 +599,7 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
|
||||
class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP heating thermostat."""
|
||||
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize heating thermostat device."""
|
||||
@@ -629,7 +628,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -680,9 +679,9 @@ class HomematicipAbsoluteHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP absolute humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ABSOLUTE_HUMIDITY
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_GRAMS_PER_CUBIC_METER
|
||||
_attr_native_unit_of_measurement = UnitOfDensity.GRAMS_PER_CUBIC_METER
|
||||
_attr_suggested_display_precision = 1
|
||||
_attr_suggested_unit_of_measurement = CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER
|
||||
_attr_suggested_unit_of_measurement = UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -1143,7 +1142,7 @@ class HomematicipSoilMoistureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP soil moisture sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.MOISTURE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
|
||||
@@ -245,11 +245,14 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
assert conn
|
||||
|
||||
def _get_info_and_disconnect() -> tuple[dict, dict]:
|
||||
result = get_device_info(conn)
|
||||
self._disconnect(conn)
|
||||
return result
|
||||
|
||||
info, wlan_settings = await self.hass.async_add_executor_job(
|
||||
get_device_info, conn
|
||||
_get_info_and_disconnect
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
await self.hass.async_add_executor_job(self._disconnect, conn)
|
||||
|
||||
user_input.update(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "huawei_lte",
|
||||
"name": "Huawei LTE",
|
||||
"codeowners": ["@scop", "@fphammerle"],
|
||||
"codeowners": ["@fphammerle"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -389,7 +389,9 @@ class InputDatetime(collection.CollectionEntity, RestoreEntity):
|
||||
|
||||
elif not self.has_time:
|
||||
extended = py_datetime.datetime.combine(
|
||||
self._current_datetime, py_datetime.time(0, 0)
|
||||
self._current_datetime,
|
||||
py_datetime.time(0, 0),
|
||||
dt_util.get_default_time_zone(),
|
||||
)
|
||||
attrs[InputDatetimeEntityStateAttribute.TIMESTAMP] = extended.timestamp()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohasupervisor.models import InterfaceMethod
|
||||
from matter_server.client import MatterClient
|
||||
from matter_server.client.exceptions import (
|
||||
CannotConnect,
|
||||
@@ -15,13 +16,20 @@ from matter_server.client.exceptions import (
|
||||
from matter_server.common.errors import MatterError, NodeNotExists
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
AddonManager,
|
||||
AddonState,
|
||||
SupervisorError,
|
||||
get_supervisor_client,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
@@ -123,6 +131,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: MatterConfigEntry) -> bo
|
||||
async_delete_issue(hass, DOMAIN, "server_version_version_too_old")
|
||||
async_delete_issue(hass, DOMAIN, "server_version_version_too_new")
|
||||
|
||||
await _async_check_ipv6_enabled(hass)
|
||||
|
||||
ble_proxy: MatterBleProxy | None = None
|
||||
|
||||
async def on_hass_stop(event: Event) -> None:
|
||||
@@ -252,6 +262,53 @@ def _derive_ble_proxy_url(matter_ws_url: str) -> str | None:
|
||||
return str(parsed.with_path(new_path))
|
||||
|
||||
|
||||
async def _async_check_ipv6_enabled(hass: HomeAssistant) -> None:
|
||||
"""Raise a repair issue when IPv6 is disabled in Supervisor network settings.
|
||||
|
||||
Matter relies on IPv6 to communicate with devices. On Supervised/HAOS
|
||||
installations the host network IPv6 method can be disabled per interface,
|
||||
which silently breaks Matter, so we surface a repair pointing the user at
|
||||
the network settings.
|
||||
"""
|
||||
if not is_hassio(hass):
|
||||
return
|
||||
|
||||
client = get_supervisor_client(hass)
|
||||
try:
|
||||
network_info = await client.network.info()
|
||||
except SupervisorError as err:
|
||||
LOGGER.debug("Failed to fetch Supervisor network info: %s", err)
|
||||
return
|
||||
|
||||
connected_interfaces = [
|
||||
interface
|
||||
for interface in network_info.interfaces
|
||||
if interface.enabled and interface.connected
|
||||
]
|
||||
# Without a connected interface we can't tell whether IPv6 is disabled or
|
||||
# the network is simply not up yet, so avoid raising a false repair.
|
||||
if not connected_interfaces:
|
||||
return
|
||||
|
||||
if any(
|
||||
interface.ipv6 is not None
|
||||
and interface.ipv6.method is not InterfaceMethod.DISABLED
|
||||
for interface in connected_interfaces
|
||||
):
|
||||
async_delete_issue(hass, DOMAIN, "ipv6_disabled")
|
||||
return
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"ipv6_disabled",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="ipv6_disabled",
|
||||
learn_more_url="homeassistant://config/network",
|
||||
)
|
||||
|
||||
|
||||
async def _client_listen(
|
||||
hass: HomeAssistant,
|
||||
entry: MatterConfigEntry,
|
||||
|
||||
@@ -721,6 +721,10 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"ipv6_disabled": {
|
||||
"description": "Matter relies on IPv6 to communicate with some devices, but IPv6 is disabled on all of your connected network interfaces. Locally connected Wi-Fi and Ethernet devices may still work, but features such as using an external Thread border router need IPv6 enabled. Select \"Learn more\" to open the network settings.",
|
||||
"title": "IPv6 is disabled but required by Matter"
|
||||
},
|
||||
"server_version_version_too_new": {
|
||||
"description": "The version of the Matter Server you are currently running is too new for this version of Home Assistant. Please update Home Assistant or downgrade the Matter Server to an older version to fix this issue.",
|
||||
"title": "Older version of Matter Server needed"
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -73,6 +74,7 @@ async def async_setup_entry(
|
||||
class MealieStatisticSensors(MealieEntity, SensorEntity):
|
||||
"""Defines a Mealie sensor."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: MealieStatisticsSensorEntityDescription
|
||||
coordinator: MealieStatisticsCoordinator
|
||||
|
||||
|
||||
@@ -15,16 +15,29 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.helpers.service_info import zeroconf
|
||||
|
||||
from .const import CONF_SERIAL, DOMAIN
|
||||
|
||||
USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
|
||||
USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): TextSelector()})
|
||||
|
||||
AUTH_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
{
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(autocomplete="username")
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="current-password"
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientError
|
||||
from pyoverkiz.action_queue import ActionQueueSettings
|
||||
from pyoverkiz.auth.credentials import (
|
||||
LocalTokenCredentials,
|
||||
RexelTokenCredentials,
|
||||
@@ -317,7 +318,9 @@ def create_local_client(
|
||||
credentials=LocalTokenCredentials(token),
|
||||
session=session,
|
||||
verify_ssl=verify_ssl,
|
||||
settings=OverkizClientSettings(default_rts_command_duration=0),
|
||||
settings=OverkizClientSettings(
|
||||
action_queue=ActionQueueSettings(), default_rts_command_duration=0
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -333,7 +336,9 @@ def create_cloud_client(
|
||||
server=server,
|
||||
credentials=UsernamePasswordCredentials(username, password),
|
||||
session=session,
|
||||
settings=OverkizClientSettings(default_rts_command_duration=0),
|
||||
settings=OverkizClientSettings(
|
||||
action_queue=ActionQueueSettings(), default_rts_command_duration=0
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -360,4 +365,5 @@ async def create_rexel_client(
|
||||
gateway_id=entry.data[CONF_GATEWAY_ID],
|
||||
),
|
||||
session=async_create_clientsession(hass),
|
||||
settings=OverkizClientSettings(action_queue=ActionQueueSettings()),
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
|
||||
self.data = {}
|
||||
self.client = client
|
||||
self.devices: dict[str, Device] = {d.device_url: d for d in devices}
|
||||
self.executions: dict[str, dict[str, str]] = {}
|
||||
self.executions: dict[str, list[dict[str, str]]] = {}
|
||||
self.areas = self._places_to_area(places) if places else None
|
||||
self._default_update_interval = UPDATE_INTERVAL
|
||||
|
||||
@@ -228,7 +228,7 @@ async def on_execution_registered(
|
||||
) -> None:
|
||||
"""Handle execution registered event."""
|
||||
if event.exec_id not in coordinator.executions:
|
||||
coordinator.executions[event.exec_id] = {}
|
||||
coordinator.executions[event.exec_id] = []
|
||||
|
||||
if not coordinator.is_stateless:
|
||||
coordinator.update_interval = timedelta(seconds=1)
|
||||
|
||||
@@ -857,7 +857,8 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
|
||||
return any(
|
||||
execution.get("device_url") == self.device.device_url
|
||||
and execution.get("command_name") == command
|
||||
for execution in self.coordinator.executions.values()
|
||||
for executions in self.coordinator.executions.values()
|
||||
for execution in executions
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -71,12 +71,15 @@ class OverkizExecutor:
|
||||
) as exception:
|
||||
raise HomeAssistantError("Failed to connect") from exception
|
||||
|
||||
# ExecutionRegisteredEvent doesn't contain the
|
||||
# device_url, thus we need to register it here
|
||||
self.coordinator.executions[exec_id] = {
|
||||
"device_url": self.device.device_url,
|
||||
"command_name": command_name,
|
||||
}
|
||||
# ExecutionRegisteredEvent doesn't contain the device_url, thus we need
|
||||
# to register it here. The action queue can merge concurrent action
|
||||
# groups under one exec_id, so accumulate rather than overwrite.
|
||||
self.coordinator.executions.setdefault(exec_id, []).append(
|
||||
{
|
||||
"device_url": self.device.device_url,
|
||||
"command_name": command_name,
|
||||
}
|
||||
)
|
||||
if refresh_afterwards:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -110,10 +113,12 @@ class OverkizExecutor:
|
||||
) as exception:
|
||||
raise HomeAssistantError("Failed to connect") from exception
|
||||
|
||||
self.coordinator.executions[exec_id] = {
|
||||
"device_url": self.device.device_url,
|
||||
"command_name": commands[-1].name,
|
||||
}
|
||||
self.coordinator.executions.setdefault(exec_id, []).append(
|
||||
{
|
||||
"device_url": self.device.device_url,
|
||||
"command_name": commands[-1].name,
|
||||
}
|
||||
)
|
||||
if refresh_afterwards:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -129,7 +134,8 @@ class OverkizExecutor:
|
||||
(
|
||||
exec_id
|
||||
# Reverse dictionary to cancel the last added execution
|
||||
for exec_id, execution in reversed(self.coordinator.executions.items())
|
||||
for exec_id, executions in reversed(self.coordinator.executions.items())
|
||||
for execution in executions
|
||||
if execution.get("device_url") == self.device.device_url
|
||||
and execution.get("command_name") in commands_to_cancel
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfInformation
|
||||
from homeassistant.const import UnitOfInformation, UnitOfRatio
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -122,7 +122,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
and data.stats.memory_stats.usage > 0
|
||||
else 0.0
|
||||
),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -151,7 +151,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
and data.stats.cpu_stats.online_cpus > 0
|
||||
else 0.0
|
||||
),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -22,15 +22,14 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfLength,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
@@ -126,7 +125,7 @@ _GAUGE_VARIANT_DESCRIPTIONS = {
|
||||
"AIRQUALITY": SensorEntityDescription(
|
||||
key="airquality",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"CURRENT": SensorEntityDescription(
|
||||
@@ -156,7 +155,7 @@ _GAUGE_VARIANT_DESCRIPTIONS = {
|
||||
"HUMIDITY": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"LIGHT": SensorEntityDescription(
|
||||
@@ -353,7 +352,7 @@ class QbusHumiditySensor(QbusEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_name = None
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@override
|
||||
@@ -382,7 +381,7 @@ class QbusVentilationSensor(QbusEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.CO2
|
||||
_attr_name = None
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PARTS_PER_MILLION
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_suggested_display_precision = 0
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
|
||||
options=[
|
||||
"always",
|
||||
"delayed",
|
||||
"delegated",
|
||||
"scheduled",
|
||||
],
|
||||
value_lambda=_get_charging_settings_mode_formatted,
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
"state": {
|
||||
"always": "Always",
|
||||
"delayed": "Delayed",
|
||||
"delegated": "Delegated",
|
||||
"scheduled": "Scheduled"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"user": {
|
||||
"menu_options": {
|
||||
"cloud": "Risco Cloud (recommended)",
|
||||
"local": "Local Risco Panel (advanced)"
|
||||
"local": "Local Risco Panel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.3.2"],
|
||||
"requirements": ["pysmlight==0.4.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import Any, override
|
||||
|
||||
import steam
|
||||
import steam.api
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import callback
|
||||
@@ -22,6 +21,14 @@ from .coordinator import SteamConfigEntry
|
||||
# To avoid too long request URIs, the amount of ids to request is limited
|
||||
MAX_IDS_TO_REQUEST = 275
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_ACCOUNT): str,
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
|
||||
def validate_input(user_input: dict[str, str]) -> dict[str, str | int]:
|
||||
"""Handle common flow input validation."""
|
||||
@@ -49,29 +56,23 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
if user_input is None and self.source == SOURCE_REAUTH:
|
||||
user_input = {CONF_ACCOUNT: self._get_reauth_entry().data[CONF_ACCOUNT]}
|
||||
elif user_input is not None:
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||
self._abort_if_unique_id_configured()
|
||||
try:
|
||||
res = await self.hass.async_add_executor_job(validate_input, user_input)
|
||||
if res is not None:
|
||||
name = str(res["personaname"])
|
||||
else:
|
||||
errors["base"] = "invalid_account"
|
||||
except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
if "403" in str(ex):
|
||||
errors["base"] = "invalid_auth"
|
||||
except steam.api.HTTPError as ex:
|
||||
errors["base"] = (
|
||||
"invalid_auth" if "403" in str(ex) else "cannot_connect"
|
||||
)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
LOGGER.exception("Unknown exception: %s", ex)
|
||||
errors["base"] = "unknown"
|
||||
if not errors:
|
||||
entry = await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||
if entry and self.source == SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_update_entry(entry, data=user_input)
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data=user_input,
|
||||
@@ -80,15 +81,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_API_KEY, default=user_input.get(CONF_API_KEY) or ""
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_ACCOUNT, default=user_input.get(CONF_ACCOUNT) or ""
|
||||
): str,
|
||||
}
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=PLACEHOLDERS,
|
||||
@@ -104,12 +98,34 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauth dialog."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_user()
|
||||
errors: dict[str, str] = {}
|
||||
entry = self._get_reauth_entry()
|
||||
|
||||
self._set_confirm_only()
|
||||
if user_input is not None:
|
||||
try:
|
||||
if not await self.hass.async_add_executor_job(
|
||||
validate_input, {**entry.data, **user_input}
|
||||
):
|
||||
errors["base"] = "invalid_account"
|
||||
except steam.api.HTTPError as ex:
|
||||
errors["base"] = (
|
||||
"invalid_auth" if "403" in str(ex) else "cannot_connect"
|
||||
)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
LOGGER.exception("Unknown exception: %s", ex)
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry, data_updates=user_input
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", description_placeholders=PLACEHOLDERS
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=PLACEHOLDERS,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,7 +134,7 @@ def _batch_ids(ids: list[str]) -> Iterator[list[str]]:
|
||||
yield ids[i : i + MAX_IDS_TO_REQUEST]
|
||||
|
||||
|
||||
class SteamOptionsFlowHandler(OptionsFlow):
|
||||
class SteamOptionsFlowHandler(OptionsFlowWithReload):
|
||||
"""Handle Steam client options."""
|
||||
|
||||
def __init__(self, entry: SteamConfigEntry) -> None:
|
||||
@@ -145,7 +161,6 @@ class SteamOptionsFlowHandler(OptionsFlow):
|
||||
if _id in user_input[CONF_ACCOUNTS]
|
||||
}
|
||||
}
|
||||
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
return self.async_create_entry(title="", data=channel_data)
|
||||
error = None
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from typing import override
|
||||
|
||||
import steam
|
||||
import steam.api
|
||||
from steam.api import _interface_method as INTMethod
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -70,7 +70,7 @@ class SteamDataUpdateCoordinator(
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self._update)
|
||||
|
||||
except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex:
|
||||
except steam.api.HTTPError as ex:
|
||||
if "401" in str(ex):
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your Steam Web API key [**here**]({api_key_url}).",
|
||||
"data": {
|
||||
"api_key": "[%key:component::steam_online::config::step::user::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::steam_online::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "The Steam integration requires re-authentication.\n\nYou can find your Steam Web API key [**here**]({api_key_url}).",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -3,23 +3,52 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Unpack, cast, override
|
||||
|
||||
import astral.sun
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_OPTIONS, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
CONF_TYPE,
|
||||
DEGREE,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.automation import (
|
||||
DomainSpec,
|
||||
move_top_level_schema_fields_to_options,
|
||||
)
|
||||
from homeassistant.helpers.condition import (
|
||||
ATTR_BEHAVIOR,
|
||||
BEHAVIOR_ANY,
|
||||
Condition,
|
||||
ConditionCheckParams,
|
||||
ConditionConfig,
|
||||
EntityNumericalConditionBase,
|
||||
condition_trace_set_result,
|
||||
condition_trace_update_result,
|
||||
)
|
||||
from homeassistant.helpers.sun import get_astral_event_date
|
||||
from homeassistant.helpers.selector import (
|
||||
NumericThresholdMode,
|
||||
NumericThresholdSelector,
|
||||
NumericThresholdSelectorConfig,
|
||||
)
|
||||
from homeassistant.helpers.sun import get_astral_event_date, get_astral_observer
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ELEVATION_ASTRONOMICAL,
|
||||
ELEVATION_CIVIL,
|
||||
ELEVATION_HORIZON,
|
||||
ELEVATION_NAUTICAL,
|
||||
STATE_ATTR_ELEVATION,
|
||||
)
|
||||
|
||||
_OPTIONS_SCHEMA_DICT: dict[vol.Marker, Any] = {
|
||||
vol.Optional("before"): cv.sun_event,
|
||||
vol.Optional("before_offset"): cv.time_period,
|
||||
@@ -167,8 +196,193 @@ class SunCondition(Condition):
|
||||
)
|
||||
|
||||
|
||||
# The sun is a singleton, so these conditions take no target and no options.
|
||||
_STATE_CONDITION_SCHEMA = vol.Schema({vol.Required(CONF_OPTIONS, default=dict): {}})
|
||||
|
||||
# The sun is a singleton, so the elevation condition always targets sun.sun
|
||||
# instead of asking the user to pick an entity.
|
||||
_SUN_ENTITY_ID = f"{DOMAIN}.{DOMAIN}"
|
||||
_ELEVATION_DOMAIN_SPECS = {DOMAIN: DomainSpec(value_source=STATE_ATTR_ELEVATION)}
|
||||
|
||||
|
||||
def _solar_position(hass: HomeAssistant) -> tuple[float, bool]:
|
||||
"""Return the sun's current elevation in degrees and whether it is rising."""
|
||||
observer = get_astral_observer(hass)
|
||||
now = dt_util.utcnow()
|
||||
elevation = astral.sun.elevation(observer, now)
|
||||
rising = astral.sun.elevation(observer, now + timedelta(minutes=1)) > elevation
|
||||
return elevation, rising
|
||||
|
||||
|
||||
class _SunStateCondition(Condition):
|
||||
"""Base class for the option-less sun state conditions."""
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, _STATE_CONDITION_SCHEMA(config))
|
||||
|
||||
|
||||
class _UpCondition(_SunStateCondition):
|
||||
"""Test if the sun is up."""
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
elevation, _ = _solar_position(self._hass)
|
||||
return elevation >= ELEVATION_HORIZON
|
||||
|
||||
|
||||
class _SetCondition(_SunStateCondition):
|
||||
"""Test if the sun is set."""
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
elevation, _ = _solar_position(self._hass)
|
||||
return elevation < ELEVATION_HORIZON
|
||||
|
||||
|
||||
class _AscendingCondition(_SunStateCondition):
|
||||
"""Test if the sun is ascending."""
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
_, rising = _solar_position(self._hass)
|
||||
return rising
|
||||
|
||||
|
||||
class _DescendingCondition(_SunStateCondition):
|
||||
"""Test if the sun is descending."""
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
_, rising = _solar_position(self._hass)
|
||||
return not rising
|
||||
|
||||
|
||||
class _NightCondition(_SunStateCondition):
|
||||
"""Test if it is night (the sun is below all twilight)."""
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
elevation, _ = _solar_position(self._hass)
|
||||
return elevation <= ELEVATION_ASTRONOMICAL
|
||||
|
||||
|
||||
_TWILIGHT_ANY = "any"
|
||||
_TWILIGHT_CIVIL = "civil"
|
||||
_TWILIGHT_NAUTICAL = "nautical"
|
||||
_TWILIGHT_ASTRONOMICAL = "astronomical"
|
||||
|
||||
# Elevation band (min, max) in degrees for each twilight type, bounded by the
|
||||
# horizon and the twilight elevations.
|
||||
_TWILIGHT_BANDS = {
|
||||
_TWILIGHT_ANY: (ELEVATION_ASTRONOMICAL, ELEVATION_HORIZON),
|
||||
_TWILIGHT_CIVIL: (ELEVATION_CIVIL, ELEVATION_HORIZON),
|
||||
_TWILIGHT_NAUTICAL: (ELEVATION_NAUTICAL, ELEVATION_CIVIL),
|
||||
_TWILIGHT_ASTRONOMICAL: (ELEVATION_ASTRONOMICAL, ELEVATION_NAUTICAL),
|
||||
}
|
||||
|
||||
_TWILIGHT_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default=dict): {
|
||||
vol.Optional(CONF_TYPE, default=_TWILIGHT_ANY): vol.In(_TWILIGHT_BANDS),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _TwilightCondition(Condition):
|
||||
"""Base class for the morning and evening twilight conditions.
|
||||
|
||||
The sun is in twilight when its elevation is within the selected band;
|
||||
morning twilight requires the sun to be rising and evening twilight to be
|
||||
descending.
|
||||
"""
|
||||
|
||||
_rising: bool
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, _TWILIGHT_CONDITION_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
super().__init__(hass, config)
|
||||
assert config.options is not None
|
||||
self._low, self._high = _TWILIGHT_BANDS[config.options[CONF_TYPE]]
|
||||
|
||||
@override
|
||||
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Check the condition."""
|
||||
elevation, rising = _solar_position(self._hass)
|
||||
return rising == self._rising and self._low <= elevation <= self._high
|
||||
|
||||
|
||||
class _MorningTwilightCondition(_TwilightCondition):
|
||||
"""Test if it is morning twilight (the sun is rising through twilight)."""
|
||||
|
||||
_rising = True
|
||||
|
||||
|
||||
class _EveningTwilightCondition(_TwilightCondition):
|
||||
"""Test if it is evening twilight (the sun is descending through twilight)."""
|
||||
|
||||
_rising = False
|
||||
|
||||
|
||||
_ELEVATION_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default=dict): {
|
||||
vol.Required("threshold"): NumericThresholdSelector(
|
||||
NumericThresholdSelectorConfig(mode=NumericThresholdMode.IS)
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _ElevationCondition(EntityNumericalConditionBase):
|
||||
"""Test the sun's elevation against a threshold."""
|
||||
|
||||
_domain_specs = _ELEVATION_DOMAIN_SPECS
|
||||
_valid_unit = DEGREE
|
||||
_schema = _ELEVATION_CONDITION_SCHEMA
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config and target the singleton sun entity."""
|
||||
config = cast(ConfigType, cls._schema(config))
|
||||
config[CONF_TARGET] = {CONF_ENTITY_ID: [_SUN_ENTITY_ID]}
|
||||
# `behavior` is needed by `EntityConditionBase.__init__`.
|
||||
config[CONF_OPTIONS][ATTR_BEHAVIOR] = BEHAVIOR_ANY
|
||||
return config
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"_": SunCondition,
|
||||
"is_up": _UpCondition,
|
||||
"is_set": _SetCondition,
|
||||
"is_ascending": _AscendingCondition,
|
||||
"is_descending": _DescendingCondition,
|
||||
"elevation": _ElevationCondition,
|
||||
"is_night": _NightCondition,
|
||||
"is_morning_twilight": _MorningTwilightCondition,
|
||||
"is_evening_twilight": _EveningTwilightCondition,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.type: &condition_type
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: twilight_type
|
||||
options:
|
||||
- any
|
||||
- civil
|
||||
- nautical
|
||||
- astronomical
|
||||
|
||||
.elevation_threshold_entity: &condition_elevation_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "°"
|
||||
- domain: number
|
||||
unit_of_measurement: "°"
|
||||
- domain: sensor
|
||||
unit_of_measurement: "°"
|
||||
|
||||
.elevation_threshold_number: &condition_elevation_threshold_number
|
||||
min: -90
|
||||
max: 90
|
||||
mode: box
|
||||
unit_of_measurement: "°"
|
||||
|
||||
is_up: {}
|
||||
is_set: {}
|
||||
is_ascending: {}
|
||||
is_descending: {}
|
||||
is_night: {}
|
||||
elevation:
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *condition_elevation_threshold_entity
|
||||
mode: is
|
||||
number: *condition_elevation_threshold_number
|
||||
is_morning_twilight:
|
||||
fields:
|
||||
type: *condition_type
|
||||
is_evening_twilight:
|
||||
fields:
|
||||
type: *condition_type
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
from typing import Final
|
||||
|
||||
import astral
|
||||
|
||||
DOMAIN: Final = "sun"
|
||||
|
||||
DEFAULT_NAME: Final = "Sun"
|
||||
|
||||
# Elevation of the sun's center at the horizon, in degrees. This is the value
|
||||
# astral uses for sunrise/sunset (atmospheric refraction plus the sun's radius).
|
||||
ELEVATION_HORIZON: Final = -0.833
|
||||
|
||||
# Sun elevation, in degrees, at each twilight boundary
|
||||
ELEVATION_CIVIL: Final[float] = -astral.Depression.CIVIL.value
|
||||
ELEVATION_NAUTICAL: Final[float] = -astral.Depression.NAUTICAL.value
|
||||
ELEVATION_ASTRONOMICAL: Final[float] = -astral.Depression.ASTRONOMICAL.value
|
||||
|
||||
SIGNAL_POSITION_CHANGED = f"{DOMAIN}_position_changed"
|
||||
SIGNAL_EVENTS_CHANGED = f"{DOMAIN}_events_changed"
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ from homeassistant.helpers.sun import (
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ELEVATION_ASTRONOMICAL,
|
||||
ELEVATION_CIVIL,
|
||||
ELEVATION_HORIZON,
|
||||
ELEVATION_NAUTICAL,
|
||||
SIGNAL_EVENTS_CHANGED,
|
||||
SIGNAL_POSITION_CHANGED,
|
||||
STATE_ABOVE_HORIZON,
|
||||
@@ -67,12 +71,8 @@ PHASE_SMALL_DAY = "small_day"
|
||||
# > 10° above horizon
|
||||
PHASE_DAY = "day"
|
||||
|
||||
# Depression angle (degrees below the horizon) of the sun at each dawn/dusk
|
||||
# phase boundary. A negative value means the sun is above the horizon.
|
||||
DEPRESSION_ASTRONOMICAL = 18.0
|
||||
DEPRESSION_NAUTICAL = 12.0
|
||||
DEPRESSION_CIVIL = 6.0
|
||||
DEPRESSION_SMALL_DAY = -10.0
|
||||
# Sun elevation (degrees above the horizon) at the start of the "small day" phase.
|
||||
_ELEVATION_SMALL_DAY = 10.0
|
||||
|
||||
# 4 mins is one degree of arc change of the sun on its circle.
|
||||
# During the night and the middle of the day we don't update
|
||||
@@ -162,8 +162,7 @@ class Sun(Entity):
|
||||
@override
|
||||
def state(self) -> str:
|
||||
"""Return the state of the sun."""
|
||||
# 0.8333 is the same value as astral uses
|
||||
if self.solar_elevation > -0.833:
|
||||
if self.solar_elevation > ELEVATION_HORIZON:
|
||||
return STATE_ABOVE_HORIZON
|
||||
|
||||
return STATE_BELOW_HORIZON
|
||||
@@ -189,8 +188,11 @@ class Sun(Entity):
|
||||
utc_point_in_time: datetime,
|
||||
sun_event: str,
|
||||
before: str | None,
|
||||
depression: float | None = None,
|
||||
elevation: float | None = None,
|
||||
) -> datetime:
|
||||
# astral takes a depression (degrees below the horizon), i.e. the
|
||||
# negated elevation.
|
||||
depression = None if elevation is None else -elevation
|
||||
next_utc = get_observer_astral_event_next(
|
||||
self.observer, sun_event, utc_point_in_time, depression=depression
|
||||
)
|
||||
@@ -209,36 +211,36 @@ class Sun(Entity):
|
||||
# Work our way around the solar cycle, figure out the next
|
||||
# phase. Some of these are stored.
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_NIGHT, DEPRESSION_ASTRONOMICAL
|
||||
utc_point_in_time, "dawn", PHASE_NIGHT, ELEVATION_ASTRONOMICAL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT, DEPRESSION_NAUTICAL
|
||||
utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT, ELEVATION_NAUTICAL
|
||||
)
|
||||
self.next_dawn = self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT, DEPRESSION_CIVIL
|
||||
utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT, ELEVATION_CIVIL
|
||||
)
|
||||
self.next_rising = self._check_event(
|
||||
utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_SMALL_DAY, DEPRESSION_SMALL_DAY
|
||||
utc_point_in_time, "dawn", PHASE_SMALL_DAY, _ELEVATION_SMALL_DAY
|
||||
)
|
||||
self.next_noon = self._check_event(utc_point_in_time, "noon", None)
|
||||
self._check_event(utc_point_in_time, "dusk", PHASE_DAY, DEPRESSION_SMALL_DAY)
|
||||
self._check_event(utc_point_in_time, "dusk", PHASE_DAY, _ELEVATION_SMALL_DAY)
|
||||
self.next_setting = self._check_event(
|
||||
utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY
|
||||
)
|
||||
self.next_dusk = self._check_event(
|
||||
utc_point_in_time, "dusk", PHASE_TWILIGHT, DEPRESSION_CIVIL
|
||||
utc_point_in_time, "dusk", PHASE_TWILIGHT, ELEVATION_CIVIL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT, DEPRESSION_NAUTICAL
|
||||
utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT, ELEVATION_NAUTICAL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time,
|
||||
"dusk",
|
||||
PHASE_ASTRONOMICAL_TWILIGHT,
|
||||
DEPRESSION_ASTRONOMICAL,
|
||||
ELEVATION_ASTRONOMICAL,
|
||||
)
|
||||
self.next_midnight = self._check_event(utc_point_in_time, "midnight", None)
|
||||
|
||||
@@ -252,11 +254,11 @@ class Sun(Entity):
|
||||
self.phase = PHASE_DAY
|
||||
elif elevation >= 0:
|
||||
self.phase = PHASE_SMALL_DAY
|
||||
elif elevation >= -6:
|
||||
elif elevation >= ELEVATION_CIVIL:
|
||||
self.phase = PHASE_TWILIGHT
|
||||
elif elevation >= -12:
|
||||
elif elevation >= ELEVATION_NAUTICAL:
|
||||
self.phase = PHASE_NAUTICAL_TWILIGHT
|
||||
elif elevation >= -18:
|
||||
elif elevation >= ELEVATION_ASTRONOMICAL:
|
||||
self.phase = PHASE_ASTRONOMICAL_TWILIGHT
|
||||
else:
|
||||
self.phase = PHASE_NIGHT
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
{
|
||||
"conditions": {
|
||||
"elevation": {
|
||||
"condition": "mdi:sun-angle"
|
||||
},
|
||||
"is_ascending": {
|
||||
"condition": "mdi:weather-sunset-up"
|
||||
},
|
||||
"is_descending": {
|
||||
"condition": "mdi:weather-sunset-down"
|
||||
},
|
||||
"is_evening_twilight": {
|
||||
"condition": "mdi:weather-sunset-down"
|
||||
},
|
||||
"is_morning_twilight": {
|
||||
"condition": "mdi:weather-sunset-up"
|
||||
},
|
||||
"is_night": {
|
||||
"condition": "mdi:weather-night"
|
||||
},
|
||||
"is_set": {
|
||||
"condition": "mdi:weather-sunny-off"
|
||||
},
|
||||
"is_up": {
|
||||
"condition": "mdi:weather-sunny"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"solar_rising": {
|
||||
|
||||
@@ -1,10 +1,62 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_for_name": "For at least",
|
||||
"trigger_threshold_name": "Threshold type",
|
||||
"twilight_type_description": "The phase of twilight.",
|
||||
"twilight_type_name": "Twilight type"
|
||||
},
|
||||
"conditions": {
|
||||
"elevation": {
|
||||
"description": "Tests the sun's elevation against a threshold you set.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::sun::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sun elevation"
|
||||
},
|
||||
"is_ascending": {
|
||||
"description": "Tests if the sun is ascending.",
|
||||
"name": "Sun is ascending"
|
||||
},
|
||||
"is_descending": {
|
||||
"description": "Tests if the sun is descending.",
|
||||
"name": "Sun is descending"
|
||||
},
|
||||
"is_evening_twilight": {
|
||||
"description": "Tests if it is evening twilight, optionally of a specific type.",
|
||||
"fields": {
|
||||
"type": {
|
||||
"description": "[%key:component::sun::common::twilight_type_description%]",
|
||||
"name": "[%key:component::sun::common::twilight_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "It is evening twilight"
|
||||
},
|
||||
"is_morning_twilight": {
|
||||
"description": "Tests if it is morning twilight, optionally of a specific type.",
|
||||
"fields": {
|
||||
"type": {
|
||||
"description": "[%key:component::sun::common::twilight_type_description%]",
|
||||
"name": "[%key:component::sun::common::twilight_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "It is morning twilight"
|
||||
},
|
||||
"is_night": {
|
||||
"description": "Tests if it is night.",
|
||||
"name": "It is night"
|
||||
},
|
||||
"is_set": {
|
||||
"description": "Tests if the sun is set.",
|
||||
"name": "Sun is set"
|
||||
},
|
||||
"is_up": {
|
||||
"description": "Tests if the sun is up.",
|
||||
"name": "Sun is up"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
@@ -45,6 +97,7 @@
|
||||
"selector": {
|
||||
"twilight_type": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"astronomical": "Astronomical",
|
||||
"civil": "Civil",
|
||||
"nautical": "Nautical"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, cast, override
|
||||
|
||||
import astral
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -47,7 +46,13 @@ from homeassistant.helpers.trigger import (
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, STATE_ATTR_ELEVATION
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ELEVATION_ASTRONOMICAL,
|
||||
ELEVATION_CIVIL,
|
||||
ELEVATION_NAUTICAL,
|
||||
STATE_ATTR_ELEVATION,
|
||||
)
|
||||
|
||||
# Names of solar events supported by the astral.sun module
|
||||
_SUN_EVENT_SOLAR_NOON = "noon"
|
||||
@@ -59,11 +64,11 @@ _TWILIGHT_CIVIL = "civil"
|
||||
_TWILIGHT_NAUTICAL = "nautical"
|
||||
_TWILIGHT_ASTRONOMICAL = "astronomical"
|
||||
|
||||
# Sun depression below the horizon for each twilight phase, as defined by astral.
|
||||
_TWILIGHT_DEPRESSIONS = {
|
||||
_TWILIGHT_CIVIL: astral.Depression.CIVIL,
|
||||
_TWILIGHT_NAUTICAL: astral.Depression.NAUTICAL,
|
||||
_TWILIGHT_ASTRONOMICAL: astral.Depression.ASTRONOMICAL,
|
||||
# Sun elevation at each twilight boundary.
|
||||
_TWILIGHT_ELEVATIONS = {
|
||||
_TWILIGHT_CIVIL: ELEVATION_CIVIL,
|
||||
_TWILIGHT_NAUTICAL: ELEVATION_NAUTICAL,
|
||||
_TWILIGHT_ASTRONOMICAL: ELEVATION_ASTRONOMICAL,
|
||||
}
|
||||
|
||||
# The sun is a singleton, so the elevation triggers always target sun.sun
|
||||
@@ -228,7 +233,7 @@ _DAWN_DUSK_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default=dict): {
|
||||
vol.Optional(CONF_TYPE, default=_TWILIGHT_CIVIL): vol.In(
|
||||
_TWILIGHT_DEPRESSIONS
|
||||
_TWILIGHT_ELEVATIONS
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -244,7 +249,7 @@ class SunDawnDuskTrigger(SunEventTrigger):
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._twilight: str = self._options[CONF_TYPE]
|
||||
self._depression = _TWILIGHT_DEPRESSIONS[self._twilight]
|
||||
self._elevation = _TWILIGHT_ELEVATIONS[self._twilight]
|
||||
|
||||
@override
|
||||
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
|
||||
@@ -252,7 +257,9 @@ class SunDawnDuskTrigger(SunEventTrigger):
|
||||
get_astral_observer(self._hass),
|
||||
self._event,
|
||||
utc_point_in_time,
|
||||
depression=self._depression,
|
||||
# astral takes a depression (degrees below the horizon), i.e. the
|
||||
# negated elevation.
|
||||
depression=-self._elevation,
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==2.2.0"]
|
||||
"requirements": ["PySwitchbot==2.3.0"]
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.11.1"]
|
||||
"requirements": ["switchbot-api==2.12.0"]
|
||||
}
|
||||
|
||||
@@ -67,12 +67,13 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
otp = user_input["otp"]
|
||||
try:
|
||||
refresh_token = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI.submit_otp, self.phone, otp
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
api = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI, refresh_token
|
||||
|
||||
def _submit_otp_and_create_api() -> tuple[str, Tami4EdgeAPI]:
|
||||
refresh_token = Tami4EdgeAPI.submit_otp(self.phone, otp)
|
||||
return refresh_token, Tami4EdgeAPI(refresh_token)
|
||||
|
||||
refresh_token, api = await self.hass.async_add_executor_job(
|
||||
_submit_otp_and_create_api
|
||||
)
|
||||
except exceptions.OTPFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -15,14 +15,8 @@ from homeassistant.components.alarm_control_panel import (
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import ATTR_CODE, CONF_NAME, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -171,6 +165,7 @@ class AbstractTemplateAlarmControlPanel(
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
_restore_state_properties = ("_attr_alarm_state",)
|
||||
|
||||
# The super init is not called because
|
||||
# TemplateEntity calls AbstractTemplateEntity.__init__.
|
||||
@@ -204,17 +199,6 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
async def _async_handle_restored_state(self) -> None:
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
and last_state.state in AlarmControlPanelState
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and self._attr_alarm_state is None
|
||||
):
|
||||
self._attr_alarm_state = AlarmControlPanelState(last_state.state)
|
||||
|
||||
async def _async_alarm_arm(self, state: Any, script: Script | None, code: Any):
|
||||
"""Arm the panel to specified state with supplied script."""
|
||||
|
||||
@@ -290,6 +274,15 @@ class AbstractTemplateAlarmControlPanel(
|
||||
code=code,
|
||||
)
|
||||
|
||||
@override
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
if last_state.state in AlarmControlPanelState:
|
||||
self._attr_alarm_state = AlarmControlPanelState(last_state.state)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlPanel):
|
||||
"""Representation of a templated Alarm Control Panel."""
|
||||
@@ -310,12 +303,6 @@ class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlP
|
||||
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, name)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
await self._async_handle_restored_state()
|
||||
|
||||
|
||||
class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControlPanel):
|
||||
"""Alarm Control Panel entity based on trigger data."""
|
||||
@@ -332,9 +319,3 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, name)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
await self._async_handle_restored_state()
|
||||
|
||||
@@ -21,10 +21,8 @@ from homeassistant.const import (
|
||||
CONF_STATE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -129,6 +127,7 @@ class AbstractTemplateBinarySensor(
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
_restore_state_properties = ("_attr_is_on",)
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
@@ -160,6 +159,12 @@ class AbstractTemplateBinarySensor(
|
||||
except vol.Invalid:
|
||||
self.setup_template(CONF_DELAY_OFF, "_delay_off", cv.positive_time_period)
|
||||
|
||||
@override
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
self._attr_is_on = last_state.state == STATE_ON
|
||||
return True
|
||||
|
||||
@callback
|
||||
@abstractmethod
|
||||
def _update_state(self, result: Any) -> None:
|
||||
@@ -181,22 +186,6 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateBinarySensor.__init__(self, config)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore state."""
|
||||
if (
|
||||
(
|
||||
CONF_DELAY_ON in self._templates
|
||||
or CONF_DELAY_OFF in self._templates
|
||||
or self._delay_on is not None
|
||||
or self._delay_off is not None
|
||||
)
|
||||
and (last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
):
|
||||
self._attr_is_on = last_state.state == STATE_ON
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
@override
|
||||
def _update_state(self, result):
|
||||
@@ -233,10 +222,49 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
|
||||
self._delay_cancel = async_call_later(self.hass, delay, _set_state)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoOffExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
|
||||
auto_off_time: datetime | None
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of additional data."""
|
||||
auto_off_time: datetime | dict[str, str] | None = self.auto_off_time
|
||||
if isinstance(auto_off_time, datetime):
|
||||
auto_off_time = {
|
||||
"__type": str(type(auto_off_time)),
|
||||
"isoformat": auto_off_time.isoformat(),
|
||||
}
|
||||
return {
|
||||
"auto_off_time": auto_off_time,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
|
||||
"""Initialize a stored binary sensor state from a dict."""
|
||||
try:
|
||||
auto_off_time = restored["auto_off_time"]
|
||||
except KeyError:
|
||||
return None
|
||||
try:
|
||||
type_ = auto_off_time["__type"]
|
||||
if type_ == "<class 'datetime.datetime'>":
|
||||
auto_off_time = dt_util.parse_datetime(auto_off_time["isoformat"])
|
||||
except TypeError:
|
||||
pass
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return cls(auto_off_time)
|
||||
|
||||
|
||||
class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
"""Sensor entity based on trigger data."""
|
||||
|
||||
domain = BINARY_SENSOR_DOMAIN
|
||||
_restore_state_extra_data = AutoOffExtraStoredData
|
||||
|
||||
# delay on and delay off are validated when the state is validated.
|
||||
skip_rendered_result = (CONF_DELAY_ON, CONF_DELAY_OFF)
|
||||
@@ -264,32 +292,19 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
self._parse_result.add(CONF_AUTO_OFF)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
def restore_extra_data(self, extra_data: AutoOffExtraStoredData) -> None:
|
||||
"""Restore extra data from the last state."""
|
||||
if CONF_AUTO_OFF not in self._config:
|
||||
return
|
||||
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and (extra_data := await self.async_get_last_binary_sensor_data())
|
||||
is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and self._attr_is_on is None
|
||||
):
|
||||
self._attr_is_on = last_state.state == STATE_ON
|
||||
self.restore_attributes(last_state)
|
||||
auto_off_time := extra_data.auto_off_time
|
||||
) is not None and auto_off_time <= dt_util.utcnow():
|
||||
# It's already past the saved auto off time
|
||||
self._attr_is_on = False
|
||||
|
||||
if CONF_AUTO_OFF not in self._config:
|
||||
return
|
||||
|
||||
if (
|
||||
auto_off_time := extra_data.auto_off_time
|
||||
) is not None and auto_off_time <= dt_util.utcnow():
|
||||
# It's already past the saved auto off time
|
||||
self._attr_is_on = False
|
||||
|
||||
if self._attr_is_on and auto_off_time is not None:
|
||||
self._set_auto_off(auto_off_time)
|
||||
if self._attr_is_on and auto_off_time is not None:
|
||||
self._set_auto_off(auto_off_time)
|
||||
|
||||
@callback
|
||||
def _cancel_delays(self):
|
||||
@@ -401,51 +416,3 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
|
||||
def extra_restore_state_data(self) -> AutoOffExtraStoredData:
|
||||
"""Return specific state data to be restored."""
|
||||
return AutoOffExtraStoredData(self._auto_off_time)
|
||||
|
||||
async def async_get_last_binary_sensor_data(
|
||||
self,
|
||||
) -> AutoOffExtraStoredData | None:
|
||||
"""Restore auto_off_time."""
|
||||
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
||||
return None
|
||||
return AutoOffExtraStoredData.from_dict(restored_last_extra_data.as_dict())
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoOffExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
|
||||
auto_off_time: datetime | None
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of additional data."""
|
||||
auto_off_time: datetime | dict[str, str] | None = self.auto_off_time
|
||||
if isinstance(auto_off_time, datetime):
|
||||
auto_off_time = {
|
||||
"__type": str(type(auto_off_time)),
|
||||
"isoformat": auto_off_time.isoformat(),
|
||||
}
|
||||
return {
|
||||
"auto_off_time": auto_off_time,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
|
||||
"""Initialize a stored binary sensor state from a dict."""
|
||||
try:
|
||||
auto_off_time = restored["auto_off_time"]
|
||||
except KeyError:
|
||||
return None
|
||||
try:
|
||||
type_ = auto_off_time["__type"]
|
||||
if type_ == "<class 'datetime.datetime'>":
|
||||
auto_off_time = dt_util.parse_datetime(auto_off_time["isoformat"])
|
||||
except TypeError:
|
||||
# native_value is not a dict
|
||||
pass
|
||||
except KeyError:
|
||||
# native_value is a dict, but does not have all values
|
||||
return None
|
||||
|
||||
return cls(auto_off_time)
|
||||
|
||||
@@ -5,15 +5,27 @@ from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ICON,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_OPTIMISTIC,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.script import Script, _VarsType
|
||||
from homeassistant.helpers.template import Template, TemplateStateFromEntityId
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DEFAULT_ENTITY_ID
|
||||
from .const import CONF_ATTRIBUTES, CONF_DEFAULT_ENTITY_ID, CONF_PICTURE
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -34,6 +46,11 @@ class AbstractTemplateEntity(Entity):
|
||||
_optimistic_entity: bool = False
|
||||
_extra_optimistic_options: tuple[str, ...] | None = None
|
||||
_state_option: str | None = None
|
||||
_restore_state_extra_data: Any | None = None
|
||||
|
||||
# Restore state properties. The state will be restored if set to None.
|
||||
# If a tuple is supplied, all properties must be None for the state to restore.
|
||||
_restore_state_properties: tuple[str, ...] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,6 +63,10 @@ class AbstractTemplateEntity(Entity):
|
||||
self._config = config
|
||||
self._templates: dict[str, EntityTemplate] = {}
|
||||
self._action_scripts: dict[str, Script] = {}
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._attribute_templates: dict[str, Template] | None = config.get(
|
||||
CONF_ATTRIBUTES
|
||||
)
|
||||
|
||||
if self._optimistic_entity:
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
@@ -200,3 +221,87 @@ class AbstractTemplateEntity(Entity):
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def _async_get_last_template_data(
|
||||
self,
|
||||
) -> Any | None:
|
||||
"""Get the last template data."""
|
||||
if self._restore_state_extra_data is None or not hasattr(
|
||||
self, "async_get_last_extra_data"
|
||||
):
|
||||
return _SENTINEL
|
||||
|
||||
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
||||
return None
|
||||
|
||||
return self._restore_state_extra_data.from_dict(
|
||||
restored_last_extra_data.as_dict()
|
||||
)
|
||||
|
||||
def restore_extra_data(self, extra_data: Any) -> None:
|
||||
"""Restore extra data from the last state."""
|
||||
|
||||
async def async_restore_last_state(self) -> None:
|
||||
"""Restore the state from the last state."""
|
||||
if not hasattr(self, "async_get_last_state"):
|
||||
return
|
||||
|
||||
last_state: State | None = await self.async_get_last_state()
|
||||
if last_state is None:
|
||||
return
|
||||
|
||||
# Handle extra data.
|
||||
extra_data = _SENTINEL
|
||||
if self._restore_state_extra_data is not None:
|
||||
extra_data = await self._async_get_last_template_data()
|
||||
|
||||
if (
|
||||
extra_data is None
|
||||
or last_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
or (
|
||||
self._restore_state_properties is not None
|
||||
and any(
|
||||
getattr(self, attr) is not None
|
||||
for attr in self._restore_state_properties
|
||||
)
|
||||
)
|
||||
):
|
||||
return
|
||||
|
||||
if not self.restore_last_state_state(last_state):
|
||||
return
|
||||
|
||||
self.restore_last_state_attributes(last_state)
|
||||
|
||||
# Extra data should be loaded last
|
||||
if extra_data is not _SENTINEL:
|
||||
self.restore_extra_data(extra_data)
|
||||
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def restore_attribute(self, conf_attr: str, attr: str, restored_value: Any) -> None:
|
||||
"""Restore an attribute from the last value."""
|
||||
|
||||
def restore_last_state_attributes(self, last_state: State) -> None:
|
||||
"""Restore attributes from the last state."""
|
||||
# Restore built-in attributes from templates
|
||||
for conf_key, attr, _attr in (
|
||||
(CONF_ICON, ATTR_ICON, "_attr_icon"),
|
||||
(CONF_NAME, ATTR_FRIENDLY_NAME, "_attr_name"),
|
||||
(CONF_PICTURE, ATTR_ENTITY_PICTURE, "_attr_entity_picture"),
|
||||
):
|
||||
if conf_key not in self._config or attr not in last_state.attributes:
|
||||
continue
|
||||
value = last_state.attributes[attr]
|
||||
self.restore_attribute(conf_key, _attr, value)
|
||||
|
||||
self._attr_extra_state_attributes = {}
|
||||
# Restore attributes from template attributes
|
||||
if self._attribute_templates:
|
||||
for attr in self._config[CONF_ATTRIBUTES]:
|
||||
if attr not in last_state.attributes:
|
||||
continue
|
||||
self._attr_extra_state_attributes[attr] = last_state.attributes[attr]
|
||||
|
||||
@@ -16,16 +16,11 @@ from homeassistant.components.sensor import (
|
||||
STATE_CLASSES_SCHEMA,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
SensorExtraStoredData,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_STATE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -186,6 +181,8 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
_restore_state_extra_data = SensorExtraStoredData
|
||||
_restore_state_properties = ("_attr_native_value",)
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
@@ -230,6 +227,13 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
|
||||
return validate_datetime(self, CONF_STATE, self.device_class)(result)
|
||||
|
||||
@override
|
||||
def restore_extra_data(self, extra_data: SensorExtraStoredData) -> None:
|
||||
"""Restore the extra data."""
|
||||
# Do not restore native_unit_of_measurement, this is always pulled from the
|
||||
# sensor configuration.
|
||||
self._attr_native_value = extra_data.native_value
|
||||
|
||||
|
||||
class StateSensorEntity(TemplateEntity, AbstractTemplateSensor):
|
||||
"""Representation of a Template Sensor."""
|
||||
@@ -262,18 +266,3 @@ class TriggerSensorEntity(TriggerEntity, AbstractTemplateSensor):
|
||||
"""Initialize."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateSensor.__init__(self, config)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and (extra_data := await self.async_get_last_sensor_data()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and CONF_STATE not in self._rendered
|
||||
):
|
||||
self._attr_native_value = extra_data.native_value
|
||||
self.restore_attributes(last_state)
|
||||
|
||||
@@ -10,14 +10,8 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import CONF_NAME, CONF_STATE, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -124,6 +118,7 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
_restore_state_properties = ("_attr_is_on",)
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
@@ -162,6 +157,12 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
self._attr_is_on = last_state.state == STATE_ON
|
||||
return True
|
||||
|
||||
|
||||
class StateSwitchEntity(TemplateEntity, AbstractTemplateSwitch):
|
||||
"""Representation of a Template switch."""
|
||||
@@ -181,16 +182,6 @@ class StateSwitchEntity(TemplateEntity, AbstractTemplateSwitch):
|
||||
assert name is not None
|
||||
AbstractTemplateSwitch.__init__(self, name, config)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
if CONF_STATE not in self._templates:
|
||||
# restore state after startup
|
||||
await super().async_added_to_hass()
|
||||
if state := await self.async_get_last_state():
|
||||
self._attr_is_on = state.state == STATE_ON
|
||||
await super().async_added_to_hass()
|
||||
|
||||
|
||||
class TriggerSwitchEntity(TriggerEntity, AbstractTemplateSwitch):
|
||||
"""Switch entity based on trigger data."""
|
||||
@@ -207,17 +198,3 @@ class TriggerSwitchEntity(TriggerEntity, AbstractTemplateSwitch):
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
AbstractTemplateSwitch.__init__(self, name, config)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and self.is_on is None
|
||||
):
|
||||
self._attr_is_on = last_state.state == STATE_ON
|
||||
self.restore_attributes(last_state)
|
||||
|
||||
@@ -43,7 +43,7 @@ from homeassistant.helpers.template import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
|
||||
from .const import CONF_AVAILABILITY, CONF_PICTURE
|
||||
from .entity import AbstractTemplateEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -161,7 +161,6 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
AbstractTemplateEntity.__init__(self, hass, config)
|
||||
self._template_attrs: dict[Template, list[_TemplateAttribute]] = {}
|
||||
self._template_result_info: TrackTemplateResultInfo | None = None
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._self_ref_update_count = 0
|
||||
self._attr_unique_id = unique_id
|
||||
self._preview_callback: (
|
||||
@@ -177,7 +176,6 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
| None
|
||||
) = None
|
||||
self._run_variables: ScriptVariables | dict
|
||||
self._attribute_templates = config.get(CONF_ATTRIBUTES)
|
||||
self._availability_template = config.get(CONF_AVAILABILITY)
|
||||
self._run_variables = config.get(CONF_VARIABLES, {})
|
||||
self._blueprint_inputs = config.get("raw_blueprint_inputs")
|
||||
@@ -569,12 +567,20 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
preview_callback(None, None, None, str(err))
|
||||
return self._call_on_remove_callbacks
|
||||
|
||||
@override
|
||||
def restore_attribute(self, conf_attr: str, attr: str, restored_value: Any) -> None:
|
||||
"""Restore an attribute from the last value."""
|
||||
setattr(self, attr, restored_value)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._async_setup_templates()
|
||||
|
||||
async_at_start(self.hass, self._async_template_startup)
|
||||
await self.async_restore_last_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Call for forced update."""
|
||||
|
||||
@@ -54,7 +54,12 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
|
||||
"""Handle being added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
if self.coordinator.data is not None:
|
||||
# The trigger already produced data; rendering it must win over
|
||||
# restored state, so skip restore entirely to avoid clobbering the
|
||||
# freshly rendered attributes.
|
||||
self._process_data()
|
||||
else:
|
||||
await self.async_restore_last_state()
|
||||
|
||||
@override
|
||||
def _set_unique_id(self, unique_id: str | None) -> None:
|
||||
@@ -151,6 +156,18 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
|
||||
|
||||
return super().available
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
# Override TriggerBaseEntity's extra_state_attributes property to restore Entity's extra state attributes behavior.
|
||||
return self._attr_extra_state_attributes
|
||||
|
||||
@override
|
||||
def restore_attribute(self, conf_attr: str, attr: str, restored_value: Any) -> None:
|
||||
"""Restore an attribute from the last value."""
|
||||
self._rendered[conf_attr] = restored_value
|
||||
|
||||
@callback
|
||||
@override
|
||||
def _render_script_variables(self) -> dict:
|
||||
@@ -209,7 +226,20 @@ class TriggerEntity( # pylint: disable=home-assistant-enforce-class-module
|
||||
self._render_single_templates(
|
||||
rendered, variables, [state_option] if state_option else []
|
||||
)
|
||||
self._render_attributes(rendered, variables)
|
||||
|
||||
if self._attribute_templates:
|
||||
attributes = {}
|
||||
for attribute, template in self._attribute_templates.items():
|
||||
try:
|
||||
value = template_render_complex(template, variables)
|
||||
attributes[attribute] = value
|
||||
variables.update({attribute: value})
|
||||
except TemplateError as err:
|
||||
log_triggered_template_error(
|
||||
self.entity_id, err, attribute=attribute
|
||||
)
|
||||
self._attr_extra_state_attributes = attributes
|
||||
|
||||
self._rendered = rendered
|
||||
|
||||
def _handle_rendered_results(self) -> bool:
|
||||
|
||||
@@ -15,13 +15,8 @@ from homeassistant.components.update import (
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_NAME,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -143,6 +138,7 @@ class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity):
|
||||
"""Representation of a template update features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_restore_state_properties = ("_attr_installed_version", "_attr_latest_version")
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
@@ -245,6 +241,13 @@ class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity):
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
@override
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
self._attr_installed_version = last_state.attributes[ATTR_INSTALLED_VERSION]
|
||||
self._attr_latest_version = last_state.attributes[ATTR_LATEST_VERSION]
|
||||
return True
|
||||
|
||||
|
||||
class StateUpdateEntity(TemplateEntity, AbstractTemplateUpdate):
|
||||
"""Representation of a Template update."""
|
||||
@@ -300,20 +303,6 @@ class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate):
|
||||
if CONF_PICTURE in config:
|
||||
self._parse_result.add(CONF_PICTURE)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
and self._attr_installed_version is None
|
||||
and self._attr_latest_version is None
|
||||
):
|
||||
self._attr_installed_version = last_state.attributes[ATTR_INSTALLED_VERSION]
|
||||
self._attr_latest_version = last_state.attributes[ATTR_LATEST_VERSION]
|
||||
self.restore_attributes(last_state)
|
||||
|
||||
@property
|
||||
@override
|
||||
def entity_picture(self) -> str | None:
|
||||
|
||||
@@ -38,10 +38,8 @@ from homeassistant.const import (
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -404,12 +402,76 @@ def validate_forecast(
|
||||
return validate
|
||||
|
||||
|
||||
class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
@dataclass(kw_only=True)
|
||||
class WeatherExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
|
||||
last_apparent_temperature: float | None
|
||||
last_cloud_coverage: int | None
|
||||
last_dew_point: float | None
|
||||
last_humidity: float | None
|
||||
last_ozone: float | None
|
||||
last_pressure: float | None
|
||||
last_temperature: float | None
|
||||
last_uv_index: float | None
|
||||
last_visibility: float | None
|
||||
last_wind_bearing: float | str | None
|
||||
last_wind_gust_speed: float | None
|
||||
last_wind_speed: float | None
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the event data."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
|
||||
"""Initialize a stored event state from a dict."""
|
||||
for key, vtypes in (
|
||||
("last_apparent_temperature", (float, int)),
|
||||
("last_cloud_coverage", (float, int)),
|
||||
("last_dew_point", (float, int)),
|
||||
("last_humidity", (float, int)),
|
||||
("last_ozone", (float, int)),
|
||||
("last_pressure", (float, int)),
|
||||
("last_temperature", (float, int)),
|
||||
("last_uv_index", (float, int)),
|
||||
("last_visibility", (float, int)),
|
||||
("last_wind_bearing", (float, int, str)),
|
||||
("last_wind_gust_speed", (float, int)),
|
||||
("last_wind_speed", (float, int)),
|
||||
):
|
||||
# This is needed to safeguard against previous restore data that has strings
|
||||
# instead of floats or ints.
|
||||
if key not in restored or (
|
||||
(value := restored[key]) is not None and not isinstance(value, vtypes)
|
||||
):
|
||||
return None
|
||||
|
||||
return cls(
|
||||
last_apparent_temperature=restored["last_apparent_temperature"],
|
||||
last_cloud_coverage=restored["last_cloud_coverage"],
|
||||
last_dew_point=restored["last_dew_point"],
|
||||
last_humidity=restored["last_humidity"],
|
||||
last_ozone=restored["last_ozone"],
|
||||
last_pressure=restored["last_pressure"],
|
||||
last_temperature=restored["last_temperature"],
|
||||
last_uv_index=restored["last_uv_index"],
|
||||
last_visibility=restored["last_visibility"],
|
||||
last_wind_bearing=restored["last_wind_bearing"],
|
||||
last_wind_gust_speed=restored["last_wind_gust_speed"],
|
||||
last_wind_speed=restored["last_wind_speed"],
|
||||
)
|
||||
|
||||
|
||||
class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity, RestoreEntity):
|
||||
"""Representation of a template weathers features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_CONDITION
|
||||
_optimistic_entity = True
|
||||
_restore_state_extra_data = WeatherExtraStoredData
|
||||
_restore_state_properties = ("_attr_condition",)
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
@@ -554,126 +616,6 @@ class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
"""Return the daily forecast in native units."""
|
||||
return self._forecast_twice_daily or []
|
||||
|
||||
|
||||
class StateWeatherEntity(TemplateEntity, AbstractTemplateWeather):
|
||||
"""Representation of a Template weather."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the Template weather."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateWeather.__init__(self, config)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class WeatherExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
|
||||
last_apparent_temperature: float | None
|
||||
last_cloud_coverage: int | None
|
||||
last_dew_point: float | None
|
||||
last_humidity: float | None
|
||||
last_ozone: float | None
|
||||
last_pressure: float | None
|
||||
last_temperature: float | None
|
||||
last_uv_index: float | None
|
||||
last_visibility: float | None
|
||||
last_wind_bearing: float | str | None
|
||||
last_wind_gust_speed: float | None
|
||||
last_wind_speed: float | None
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the event data."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
|
||||
"""Initialize a stored event state from a dict."""
|
||||
for key, vtypes in (
|
||||
("last_apparent_temperature", (float, int)),
|
||||
("last_cloud_coverage", (float, int)),
|
||||
("last_dew_point", (float, int)),
|
||||
("last_humidity", (float, int)),
|
||||
("last_ozone", (float, int)),
|
||||
("last_pressure", (float, int)),
|
||||
("last_temperature", (float, int)),
|
||||
("last_uv_index", (float, int)),
|
||||
("last_visibility", (float, int)),
|
||||
("last_wind_bearing", (float, int, str)),
|
||||
("last_wind_gust_speed", (float, int)),
|
||||
("last_wind_speed", (float, int)),
|
||||
):
|
||||
# This is needed to safeguard against previous restore data that has strings
|
||||
# instead of floats or ints.
|
||||
if key not in restored or (
|
||||
(value := restored[key]) is not None and not isinstance(value, vtypes)
|
||||
):
|
||||
return None
|
||||
|
||||
return cls(
|
||||
last_apparent_temperature=restored["last_apparent_temperature"],
|
||||
last_cloud_coverage=restored["last_cloud_coverage"],
|
||||
last_dew_point=restored["last_dew_point"],
|
||||
last_humidity=restored["last_humidity"],
|
||||
last_ozone=restored["last_ozone"],
|
||||
last_pressure=restored["last_pressure"],
|
||||
last_temperature=restored["last_temperature"],
|
||||
last_uv_index=restored["last_uv_index"],
|
||||
last_visibility=restored["last_visibility"],
|
||||
last_wind_bearing=restored["last_wind_bearing"],
|
||||
last_wind_gust_speed=restored["last_wind_gust_speed"],
|
||||
last_wind_speed=restored["last_wind_speed"],
|
||||
)
|
||||
|
||||
|
||||
class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather, RestoreEntity):
|
||||
"""Weather entity based on trigger data."""
|
||||
|
||||
domain = WEATHER_DOMAIN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateWeather.__init__(self, config)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if (
|
||||
(state := await self.async_get_last_state())
|
||||
and state.state is not None
|
||||
and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
and (weather_data := await self.async_get_last_weather_data())
|
||||
):
|
||||
self._attr_native_apparent_temperature = (
|
||||
weather_data.last_apparent_temperature
|
||||
)
|
||||
self._attr_cloud_coverage = weather_data.last_cloud_coverage
|
||||
self._attr_condition = state.state
|
||||
self._attr_native_dew_point = weather_data.last_dew_point
|
||||
self._attr_humidity = weather_data.last_humidity
|
||||
self._attr_ozone = weather_data.last_ozone
|
||||
self._attr_native_pressure = weather_data.last_pressure
|
||||
self._attr_native_temperature = weather_data.last_temperature
|
||||
self._attr_uv_index = weather_data.last_uv_index
|
||||
self._attr_native_visibility = weather_data.last_visibility
|
||||
self._attr_wind_bearing = weather_data.last_wind_bearing
|
||||
self._attr_native_wind_gust_speed = weather_data.last_wind_gust_speed
|
||||
self._attr_native_wind_speed = weather_data.last_wind_speed
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_restore_state_data(self) -> WeatherExtraStoredData:
|
||||
@@ -693,8 +635,56 @@ class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather, RestoreEntity
|
||||
last_wind_speed=self.native_wind_speed,
|
||||
)
|
||||
|
||||
async def async_get_last_weather_data(self) -> WeatherExtraStoredData | None:
|
||||
"""Restore weather specific state data."""
|
||||
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
||||
return None
|
||||
return WeatherExtraStoredData.from_dict(restored_last_extra_data.as_dict())
|
||||
@override
|
||||
def restore_last_state_state(self, last_state: State) -> bool:
|
||||
"""Restore the state from the last state."""
|
||||
self._attr_condition = last_state.state
|
||||
return True
|
||||
|
||||
@override
|
||||
def restore_extra_data(self, extra_data: WeatherExtraStoredData) -> None:
|
||||
"""Restore the extra data."""
|
||||
self._attr_native_apparent_temperature = extra_data.last_apparent_temperature
|
||||
self._attr_cloud_coverage = extra_data.last_cloud_coverage
|
||||
self._attr_native_dew_point = extra_data.last_dew_point
|
||||
self._attr_humidity = extra_data.last_humidity
|
||||
self._attr_ozone = extra_data.last_ozone
|
||||
self._attr_native_pressure = extra_data.last_pressure
|
||||
self._attr_native_temperature = extra_data.last_temperature
|
||||
self._attr_uv_index = extra_data.last_uv_index
|
||||
self._attr_native_visibility = extra_data.last_visibility
|
||||
self._attr_wind_bearing = extra_data.last_wind_bearing
|
||||
self._attr_native_wind_gust_speed = extra_data.last_wind_gust_speed
|
||||
self._attr_native_wind_speed = extra_data.last_wind_speed
|
||||
|
||||
|
||||
class StateWeatherEntity(TemplateEntity, AbstractTemplateWeather):
|
||||
"""Representation of a Template weather."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the Template weather."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateWeather.__init__(self, config)
|
||||
|
||||
|
||||
class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather):
|
||||
"""Weather entity based on trigger data."""
|
||||
|
||||
domain = WEATHER_DOMAIN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateWeather.__init__(self, config)
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==14.0.0"]
|
||||
"requirements": ["uiprotect==15.0.0"]
|
||||
}
|
||||
|
||||
@@ -62,10 +62,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeraConfigEntry) -> bool
|
||||
controller = veraApi.VeraController(base_url, subscription_registry)
|
||||
|
||||
try:
|
||||
all_devices = await hass.async_add_executor_job(controller.get_devices)
|
||||
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
all_scenes = await hass.async_add_executor_job(controller.get_scenes)
|
||||
def _get_devices_and_scenes():
|
||||
"""Get devices and scenes from the Vera controller."""
|
||||
return controller.get_devices(), controller.get_scenes()
|
||||
|
||||
all_devices, all_scenes = await hass.async_add_executor_job(
|
||||
_get_devices_and_scenes
|
||||
)
|
||||
except RequestException as exception:
|
||||
# There was a network related error connecting to the Vera controller.
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
|
||||
@@ -20,16 +20,16 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfEnergy,
|
||||
UnitOfMass,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
@@ -80,7 +80,7 @@ VICARE_UNIT_TO_HA_UNIT = {
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_PERCENT: PERCENTAGE,
|
||||
VICARE_PERCENT: UnitOfRatio.PERCENTAGE,
|
||||
VICARE_W: UnitOfPower.WATT,
|
||||
VICARE_WH: UnitOfEnergy.WATT_HOUR,
|
||||
}
|
||||
@@ -117,7 +117,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="outside_humidity",
|
||||
translation_key="outside_humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getOutsideHumidity(),
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -165,7 +165,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="primary_circuit_pump_rotation",
|
||||
translation_key="primary_circuit_pump_rotation",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getPrimaryCircuitPumpRotation(),
|
||||
unit_getter=lambda api: api.getPrimaryCircuitPumpRotationUnit(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -799,7 +799,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="ess_state_of_charge",
|
||||
translation_key="ess_state_of_charge",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getElectricalEnergySystemSOC(),
|
||||
@@ -996,7 +996,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="room_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getHumidity(),
|
||||
),
|
||||
@@ -1122,7 +1122,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="battery_level",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -1142,7 +1142,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
translation_key="zigbee_signal_strength",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getZigbeeSignalStrength(),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -1150,7 +1150,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
key="valve_position",
|
||||
translation_key="valve_position",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getValvePosition(),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -1177,7 +1177,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
key="supply_humidity",
|
||||
translation_key="supply_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getSupplyHumidity(),
|
||||
),
|
||||
@@ -1229,28 +1229,28 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm01",
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getAirborneDustPM1(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm02",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getAirborneDustPM2d5(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm04",
|
||||
device_class=SensorDeviceClass.PM4,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getAirborneDustPM4(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getAirborneDustPM10(),
|
||||
),
|
||||
@@ -1293,7 +1293,7 @@ BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="burner_modulation",
|
||||
translation_key="burner_modulation",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getModulation(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@@ -1312,7 +1312,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="compressor_modulation",
|
||||
translation_key="compressor_modulation",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
value_getter=lambda api: api.getModulation(),
|
||||
unit_getter=lambda api: api.getModulationUnit(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==2.0.0", "zha-quirks==2.0.0"],
|
||||
"requirements": ["zha==2.0.0", "zha-quirks==2.1.0"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -407,6 +407,9 @@
|
||||
"reset_alarm": {
|
||||
"name": "Reset alarm"
|
||||
},
|
||||
"reset_energy": {
|
||||
"name": "Reset energy"
|
||||
},
|
||||
"reset_frost_lock": {
|
||||
"name": "Frost lock reset"
|
||||
},
|
||||
@@ -1387,6 +1390,12 @@
|
||||
"status_indication": {
|
||||
"name": "Status indication"
|
||||
},
|
||||
"switch_action_l1": {
|
||||
"name": "Switch action L1"
|
||||
},
|
||||
"switch_action_l2": {
|
||||
"name": "Switch action L2"
|
||||
},
|
||||
"switch_actions": {
|
||||
"name": "Switch actions"
|
||||
},
|
||||
@@ -1399,6 +1408,12 @@
|
||||
"switch_type": {
|
||||
"name": "Switch type"
|
||||
},
|
||||
"switch_type_l1": {
|
||||
"name": "Switch type L1"
|
||||
},
|
||||
"switch_type_l2": {
|
||||
"name": "Switch type L2"
|
||||
},
|
||||
"temperature_display_mode": {
|
||||
"name": "Temperature display mode"
|
||||
},
|
||||
|
||||
@@ -455,7 +455,7 @@
|
||||
"name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Bulk set partial configuration parameters (advanced)"
|
||||
"name": "Bulk set partial configuration parameters"
|
||||
},
|
||||
"clear_lock_usercode": {
|
||||
"description": "Clears a user code from a lock.",
|
||||
@@ -559,7 +559,7 @@
|
||||
"name": "Parameters"
|
||||
}
|
||||
},
|
||||
"name": "Invoke a Command Class API on a node (advanced)"
|
||||
"name": "Invoke a Command Class API on a node"
|
||||
},
|
||||
"multicast_set_value": {
|
||||
"description": "Changes any value that Z-Wave recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This action has minimal validation so only use this action if you know what you are doing.",
|
||||
@@ -605,7 +605,7 @@
|
||||
"name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Set a value on multiple devices via multicast (advanced)"
|
||||
"name": "Set a value on multiple devices via multicast"
|
||||
},
|
||||
"ping": {
|
||||
"description": "Forces Z-Wave to try to reach a node. This can be used to update the status of the node in Z-Wave when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep.",
|
||||
@@ -649,7 +649,7 @@
|
||||
"name": "Notification Type"
|
||||
}
|
||||
},
|
||||
"name": "Refresh notifications on a node (advanced)"
|
||||
"name": "Refresh notifications on a node"
|
||||
},
|
||||
"refresh_value": {
|
||||
"description": "Force updates the values of a Z-Wave entity.",
|
||||
@@ -869,7 +869,7 @@
|
||||
"name": "Wait for result"
|
||||
}
|
||||
},
|
||||
"name": "Set a value (advanced)"
|
||||
"name": "Set a value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 7
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
@@ -455,6 +455,26 @@ ATTR_TEMPERATURE: Final = "temperature"
|
||||
ATTR_PERSONS: Final = "persons"
|
||||
|
||||
|
||||
class EntityCapabilityAttribute(StrEnum):
|
||||
"""Capability attributes shared by all entities."""
|
||||
|
||||
GROUP_ENTITIES = "group_entities"
|
||||
|
||||
|
||||
class EntityStateAttribute(StrEnum):
|
||||
"""State attributes shared by all entities."""
|
||||
|
||||
ASSUMED_STATE = "assumed_state"
|
||||
ATTRIBUTION = "attribution"
|
||||
DEVICE_CLASS = "device_class"
|
||||
ENTITY_PICTURE = "entity_picture"
|
||||
FRIENDLY_NAME = "friendly_name"
|
||||
ICON = "icon"
|
||||
RESTORED = "restored"
|
||||
SUPPORTED_FEATURES = "supported_features"
|
||||
UNIT_OF_MEASUREMENT = "unit_of_measurement"
|
||||
|
||||
|
||||
# #### UNITS OF MEASUREMENT ####
|
||||
# Apparent power units
|
||||
class UnitOfApparentPower(StrEnum):
|
||||
|
||||
@@ -653,12 +653,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"avion": {
|
||||
"name": "Avi-on",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "assumed_state"
|
||||
},
|
||||
"avosdim": {
|
||||
"name": "Avosdim",
|
||||
"integration_type": "virtual",
|
||||
@@ -735,12 +729,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"beewi_smartclim": {
|
||||
"name": "BeeWi SmartClim BLE sensor",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"bega": {
|
||||
"name": "BEGA",
|
||||
"iot_standards": [
|
||||
|
||||
@@ -30,21 +30,14 @@ from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_GROUP_ENTITIES,
|
||||
ATTR_ICON,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_DEFAULT_NAME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
EntityCapabilityAttribute,
|
||||
EntityCategory,
|
||||
EntityStateAttribute,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
@@ -167,7 +160,7 @@ def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
First try the statemachine, then entity registry.
|
||||
"""
|
||||
if state := hass.states.get(entity_id):
|
||||
return state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
return state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
if not (entry := entity_registry.async_get(entity_id)):
|
||||
@@ -192,7 +185,7 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int:
|
||||
First try the statemachine, then entity registry.
|
||||
"""
|
||||
if state := hass.states.get(entity_id):
|
||||
return state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # type: ignore[no-any-return]
|
||||
return state.attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) # type: ignore[no-any-return]
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
if not (entry := entity_registry.async_get(entity_id)):
|
||||
@@ -207,7 +200,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
First try the statemachine, then entity registry.
|
||||
"""
|
||||
if state := hass.states.get(entity_id):
|
||||
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
return state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
if not (entry := entity_registry.async_get(entity_id)):
|
||||
@@ -1116,7 +1109,9 @@ class Entity(
|
||||
capability_attr = self.capability_attributes
|
||||
if self.__group is not None:
|
||||
capability_attr = capability_attr.copy() if capability_attr else {}
|
||||
capability_attr[ATTR_GROUP_ENTITIES] = self.__group.member_entity_ids.copy()
|
||||
capability_attr[EntityCapabilityAttribute.GROUP_ENTITIES] = (
|
||||
self.__group.member_entity_ids.copy()
|
||||
)
|
||||
|
||||
attr = capability_attr.copy() if capability_attr else {}
|
||||
|
||||
@@ -1129,25 +1124,25 @@ class Entity(
|
||||
attr |= extra_state_attributes
|
||||
|
||||
if (unit_of_measurement := self.unit_of_measurement) is not None:
|
||||
attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement
|
||||
attr[EntityStateAttribute.UNIT_OF_MEASUREMENT] = unit_of_measurement
|
||||
|
||||
if assumed_state := self.assumed_state:
|
||||
attr[ATTR_ASSUMED_STATE] = assumed_state
|
||||
attr[EntityStateAttribute.ASSUMED_STATE] = assumed_state
|
||||
|
||||
if (attribution := self.attribution) is not None:
|
||||
attr[ATTR_ATTRIBUTION] = attribution
|
||||
attr[EntityStateAttribute.ATTRIBUTION] = attribution
|
||||
|
||||
original_device_class = self.device_class
|
||||
if (
|
||||
device_class := (entry and entry.device_class) or original_device_class
|
||||
) is not None:
|
||||
attr[ATTR_DEVICE_CLASS] = str(device_class)
|
||||
attr[EntityStateAttribute.DEVICE_CLASS] = str(device_class)
|
||||
|
||||
if (entity_picture := self.entity_picture) is not None:
|
||||
attr[ATTR_ENTITY_PICTURE] = entity_picture
|
||||
attr[EntityStateAttribute.ENTITY_PICTURE] = entity_picture
|
||||
|
||||
if (icon := (entry and entry.icon) or self.icon) is not None:
|
||||
attr[ATTR_ICON] = icon
|
||||
attr[EntityStateAttribute.ICON] = icon
|
||||
|
||||
original_name = self.name
|
||||
if original_name is UNDEFINED:
|
||||
@@ -1169,10 +1164,10 @@ class Entity(
|
||||
self._cached_friendly_name = (original_name, name)
|
||||
|
||||
if name:
|
||||
attr[ATTR_FRIENDLY_NAME] = name
|
||||
attr[EntityStateAttribute.FRIENDLY_NAME] = name
|
||||
|
||||
if (supported_features := self.supported_features) is not None:
|
||||
attr[ATTR_SUPPORTED_FEATURES] = supported_features
|
||||
attr[EntityStateAttribute.SUPPORTED_FEATURES] = supported_features
|
||||
|
||||
return (
|
||||
state,
|
||||
|
||||
@@ -32,6 +32,7 @@ from homeassistant.exceptions import (
|
||||
PlatformNotReady,
|
||||
)
|
||||
from homeassistant.generated import languages
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.setup import SetupPhases, async_start_setup
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
@@ -53,6 +54,8 @@ SLOW_SETUP_MAX_WAIT = 60
|
||||
SLOW_ADD_ENTITY_MAX_WAIT = 15 # Per Entity
|
||||
SLOW_ADD_MIN_TIMEOUT = 500
|
||||
|
||||
MAX_ENABLED_ENTITIES_PER_CONFIG_ENTRY = 10000
|
||||
|
||||
PLATFORM_NOT_READY_RETRIES = 10
|
||||
DATA_ENTITY_PLATFORM: HassKey[dict[str, list[EntityPlatform]]] = HassKey(
|
||||
"entity_platform"
|
||||
@@ -276,6 +279,8 @@ class EntityPlatform:
|
||||
# Storage for entities for this specific platform only
|
||||
# which are indexed by entity_id
|
||||
self.entities: dict[str, Entity] = {}
|
||||
# Whether we already warned about reaching the config entry entity limit
|
||||
self._entity_limit_warned = False
|
||||
self._tasks: list[asyncio.Task[None]] = []
|
||||
# Stop tracking tasks after setup is completed
|
||||
self._setup_complete = False
|
||||
@@ -959,6 +964,32 @@ class EntityPlatform:
|
||||
if not entity.entity_registry_visible_default:
|
||||
hidden_by = RegistryEntryHider.INTEGRATION
|
||||
|
||||
if (
|
||||
disabled_by is None
|
||||
and not registered_entity_id
|
||||
and self.config_entry is not None
|
||||
and entity_registry.entities.get_enabled_count_for_config_entry_id(
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
>= MAX_ENABLED_ENTITIES_PER_CONFIG_ENTRY
|
||||
):
|
||||
if not self._entity_limit_warned:
|
||||
report_issue = async_suggest_report_issue(
|
||||
self.hass, integration_domain=self.platform_name
|
||||
)
|
||||
self.logger.warning(
|
||||
"Reached the maximum of %s enabled entities for config entry "
|
||||
"%s; not adding more entities for integration %s until "
|
||||
"existing entities are removed or disabled, please %s",
|
||||
MAX_ENABLED_ENTITIES_PER_CONFIG_ENTRY,
|
||||
self.config_entry.entry_id,
|
||||
self.platform_name,
|
||||
report_issue,
|
||||
)
|
||||
self._entity_limit_warned = True
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
self.domain,
|
||||
self.platform_name,
|
||||
@@ -1054,6 +1085,7 @@ class EntityPlatform:
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
self.async_cancel_retry_setup()
|
||||
self._entity_limit_warned = False
|
||||
|
||||
if not self.entities:
|
||||
return
|
||||
|
||||
@@ -20,18 +20,13 @@ import attr
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ICON,
|
||||
ATTR_RESTORED,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
MAX_LENGTH_STATE_DOMAIN,
|
||||
MAX_LENGTH_STATE_ENTITY_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
EntityCategory,
|
||||
EntityStateAttribute,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -429,28 +424,28 @@ class RegistryEntry:
|
||||
@callback
|
||||
def write_unavailable_state(self, hass: HomeAssistant) -> None:
|
||||
"""Write the unavailable state to the state machine."""
|
||||
attrs: dict[str, Any] = {ATTR_RESTORED: True}
|
||||
attrs: dict[str, Any] = {EntityStateAttribute.RESTORED: True}
|
||||
|
||||
if self.capabilities is not None:
|
||||
attrs.update(self.capabilities)
|
||||
|
||||
device_class = self.device_class or self.original_device_class
|
||||
if device_class is not None:
|
||||
attrs[ATTR_DEVICE_CLASS] = device_class
|
||||
attrs[EntityStateAttribute.DEVICE_CLASS] = device_class
|
||||
|
||||
icon = self.icon or self.original_icon
|
||||
if icon is not None:
|
||||
attrs[ATTR_ICON] = icon
|
||||
attrs[EntityStateAttribute.ICON] = icon
|
||||
|
||||
name = self.name or self.original_name
|
||||
if name is not None:
|
||||
attrs[ATTR_FRIENDLY_NAME] = name
|
||||
name = async_get_full_entity_name(hass, self)
|
||||
if name:
|
||||
attrs[EntityStateAttribute.FRIENDLY_NAME] = name
|
||||
|
||||
if self.supported_features is not None:
|
||||
attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features
|
||||
attrs[EntityStateAttribute.SUPPORTED_FEATURES] = self.supported_features
|
||||
|
||||
if self.unit_of_measurement is not None:
|
||||
attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
|
||||
attrs[EntityStateAttribute.UNIT_OF_MEASUREMENT] = self.unit_of_measurement
|
||||
|
||||
hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
|
||||
|
||||
@@ -935,6 +930,8 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]):
|
||||
- device_id -> dict[key, True]
|
||||
- area_id -> dict[key, True]
|
||||
- label -> dict[key, True]
|
||||
|
||||
Also maintains a count of enabled entries per config entry id.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -943,6 +940,7 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]):
|
||||
self._entry_ids: dict[str, RegistryEntry] = {}
|
||||
self._index: dict[tuple[str, str, str], str] = {}
|
||||
self._config_entry_id_index: RegistryIndexType = defaultdict(dict)
|
||||
self._config_entry_id_enabled_count: dict[str, int] = {}
|
||||
self._device_id_index: RegistryIndexType = defaultdict(dict)
|
||||
self._area_id_index: RegistryIndexType = defaultdict(dict)
|
||||
self._labels_index: RegistryIndexType = defaultdict(dict)
|
||||
@@ -956,6 +954,10 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]):
|
||||
# https://discuss.python.org/t/add-orderedset-to-stdlib/12730
|
||||
if (config_entry_id := entry.config_entry_id) is not None:
|
||||
self._config_entry_id_index[config_entry_id][key] = True
|
||||
if not entry.disabled_by:
|
||||
self._config_entry_id_enabled_count[config_entry_id] = (
|
||||
self._config_entry_id_enabled_count.get(config_entry_id, 0) + 1
|
||||
)
|
||||
if (device_id := entry.device_id) is not None:
|
||||
self._device_id_index[device_id][key] = True
|
||||
if (area_id := entry.area_id) is not None:
|
||||
@@ -973,6 +975,12 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]):
|
||||
del self._index[(entry.domain, entry.platform, entry.unique_id)]
|
||||
if config_entry_id := entry.config_entry_id:
|
||||
self._unindex_entry_value(key, config_entry_id, self._config_entry_id_index)
|
||||
if not entry.disabled_by:
|
||||
count = self._config_entry_id_enabled_count[config_entry_id] - 1
|
||||
if count:
|
||||
self._config_entry_id_enabled_count[config_entry_id] = count
|
||||
else:
|
||||
del self._config_entry_id_enabled_count[config_entry_id]
|
||||
if device_id := entry.device_id:
|
||||
self._unindex_entry_value(key, device_id, self._device_id_index)
|
||||
if area_id := entry.area_id:
|
||||
@@ -1013,6 +1021,10 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]):
|
||||
data[key] for key in self._config_entry_id_index.get(config_entry_id, ())
|
||||
]
|
||||
|
||||
def get_enabled_count_for_config_entry_id(self, config_entry_id: str) -> int:
|
||||
"""Return the number of enabled entries for a config entry."""
|
||||
return self._config_entry_id_enabled_count.get(config_entry_id, 0)
|
||||
|
||||
def get_entries_for_area_id(self, area_id: str) -> list[RegistryEntry]:
|
||||
"""Get entries for area."""
|
||||
data = self.data
|
||||
@@ -2438,7 +2450,9 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -
|
||||
if event.data["action"] == "update":
|
||||
old_entity_id = event.data["old_entity_id"]
|
||||
old_state = hass.states.get(old_entity_id)
|
||||
if old_state is None or not old_state.attributes.get(ATTR_RESTORED):
|
||||
if old_state is None or not old_state.attributes.get(
|
||||
EntityStateAttribute.RESTORED
|
||||
):
|
||||
return
|
||||
hass.states.async_remove(old_entity_id, context=event.context)
|
||||
if entry := registry.async_get(event.data["entity_id"]):
|
||||
@@ -2447,7 +2461,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -
|
||||
|
||||
state = hass.states.get(event.data["entity_id"])
|
||||
|
||||
if state is None or not state.attributes.get(ATTR_RESTORED):
|
||||
if state is None or not state.attributes.get(EntityStateAttribute.RESTORED):
|
||||
return
|
||||
|
||||
hass.states.async_remove(event.data["entity_id"], context=event.context)
|
||||
|
||||
@@ -27,7 +27,7 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
|
||||
@@ -246,21 +246,6 @@ class TriggerBaseEntity(Entity):
|
||||
"""Set unique id."""
|
||||
self._unique_id = unique_id
|
||||
|
||||
def restore_attributes(self, last_state: State) -> None:
|
||||
"""Restore attributes."""
|
||||
for conf_key, attr in CONF_TO_ATTRIBUTE.items():
|
||||
if conf_key not in self._config or attr not in last_state.attributes:
|
||||
continue
|
||||
self._rendered[conf_key] = last_state.attributes[attr]
|
||||
|
||||
if CONF_ATTRIBUTES in self._config:
|
||||
extra_state_attributes = {}
|
||||
for attr in self._config[CONF_ATTRIBUTES]:
|
||||
if attr not in last_state.attributes:
|
||||
continue
|
||||
extra_state_attributes[attr] = last_state.attributes[attr]
|
||||
self._rendered[CONF_ATTRIBUTES] = extra_state_attributes
|
||||
|
||||
def _template_variables(self, run_variables: dict[str, Any] | None = None) -> dict:
|
||||
"""Render template variables."""
|
||||
return {
|
||||
|
||||
@@ -29,18 +29,18 @@ cached-ipaddress==1.1.2
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==48.0.1
|
||||
cryptography==49.0.0
|
||||
dbus-fast==5.0.22
|
||||
file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.23.1
|
||||
habluetooth==6.25.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.8.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260527.7
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-frontend==20260624.0
|
||||
home-assistant-intents==2026.6.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
@@ -56,7 +56,7 @@ psutil-home-assistant==0.0.1
|
||||
PyJWT==2.12.1
|
||||
pymicro-vad==1.0.1
|
||||
PyNaCl==1.6.2
|
||||
pyOpenSSL==26.2.0
|
||||
pyOpenSSL==26.3.0
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.3
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260527.7"
|
||||
FRONTEND_VERSION: Final[str] = "20260624.0"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.7.0.dev0"
|
||||
version = "2026.8.0.dev0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -57,10 +57,10 @@ dependencies = [
|
||||
"lru-dict==1.4.1",
|
||||
"PyJWT==2.12.1",
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
"cryptography==48.0.1",
|
||||
"cryptography==49.0.0",
|
||||
"Pillow==12.2.0",
|
||||
"propcache==0.5.2",
|
||||
"pyOpenSSL==26.2.0",
|
||||
"pyOpenSSL==26.3.0",
|
||||
"orjson==3.11.9",
|
||||
"packaging>=23.1",
|
||||
"psutil-home-assistant==0.0.1",
|
||||
|
||||
Generated
+3
-3
@@ -21,13 +21,13 @@ bcrypt==5.0.0
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==48.0.1
|
||||
cryptography==49.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.8.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-intents==2026.6.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
infrared-protocols==6.3.0
|
||||
@@ -41,7 +41,7 @@ propcache==0.5.2
|
||||
psutil-home-assistant==0.0.1
|
||||
PyJWT==2.12.1
|
||||
pymicro-vad==1.0.1
|
||||
pyOpenSSL==26.2.0
|
||||
pyOpenSSL==26.3.0
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.3
|
||||
|
||||
Generated
+9
-15
@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
|
||||
PySrDaliGateway==0.21.0
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==2.2.0
|
||||
PySwitchbot==2.3.0
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@@ -608,9 +608,6 @@ av==17.0.1
|
||||
# homeassistant.components.avea
|
||||
avea==1.8.0
|
||||
|
||||
# homeassistant.components.avion
|
||||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==72
|
||||
|
||||
@@ -650,9 +647,6 @@ batinfo==0.4.2
|
||||
# homeassistant.components.scrape
|
||||
beautifulsoup4==4.13.3
|
||||
|
||||
# homeassistant.components.beewi_smartclim
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
|
||||
@@ -1225,7 +1219,7 @@ ha-xthings-cloud==1.0.5
|
||||
habiticalib==0.4.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==6.23.1
|
||||
habluetooth==6.25.1
|
||||
|
||||
# homeassistant.components.hanna
|
||||
hanna-cloud==0.0.7
|
||||
@@ -1278,10 +1272,10 @@ hole==0.9.2
|
||||
holidays==0.99
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260527.7
|
||||
home-assistant-frontend==20260624.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-intents==2026.6.24
|
||||
|
||||
# homeassistant.components.homekit
|
||||
homekit-audio-proxy==1.2.1
|
||||
@@ -1290,7 +1284,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.5
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.13.1
|
||||
homematicip==2.13.2
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
@@ -2576,7 +2570,7 @@ pysmhi==2.0.0
|
||||
pysml==0.1.8
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.3.2
|
||||
pysmlight==0.4.0
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp==7.1.27
|
||||
@@ -3120,7 +3114,7 @@ surepy==0.9.0
|
||||
swisshydrodata==0.1.0
|
||||
|
||||
# homeassistant.components.switchbot_cloud
|
||||
switchbot-api==2.11.1
|
||||
switchbot-api==2.12.0
|
||||
|
||||
# homeassistant.components.synology_srm
|
||||
synology-srm==0.2.0
|
||||
@@ -3251,7 +3245,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==14.0.0
|
||||
uiprotect==15.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.6.1
|
||||
@@ -3459,7 +3453,7 @@ zeroconf==0.150.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==2.0.0
|
||||
zha-quirks==2.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==2.0.0
|
||||
|
||||
@@ -20,7 +20,6 @@ from script.hassfest.model import Config, Integration
|
||||
# requirements_all.txt.
|
||||
EXCLUDED_REQUIREMENTS_ALL = {
|
||||
"atenpdu", # depends on pysnmp which is not maintained at this time
|
||||
"avion",
|
||||
"beewi-smartclim", # depends on bluepy
|
||||
"bluepy",
|
||||
"evdev",
|
||||
|
||||
@@ -172,7 +172,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"aurora_abb_powerone",
|
||||
"aussie_broadband",
|
||||
"avea",
|
||||
"avion",
|
||||
"aws",
|
||||
"axis",
|
||||
"azure_data_explorer",
|
||||
@@ -186,7 +185,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"bang_olufsen",
|
||||
"bayesian",
|
||||
"bbox",
|
||||
"beewi_smartclim",
|
||||
"bitcoin",
|
||||
"bizkaibus",
|
||||
"blackbird",
|
||||
@@ -1123,7 +1121,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"aurora_abb_powerone",
|
||||
"aussie_broadband",
|
||||
"avea",
|
||||
"avion",
|
||||
"awair",
|
||||
"aws",
|
||||
"axis",
|
||||
@@ -1138,7 +1135,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"bang_olufsen",
|
||||
"bayesian",
|
||||
"bbox",
|
||||
"beewi_smartclim",
|
||||
"bitcoin",
|
||||
"bizkaibus",
|
||||
"blackbird",
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
# name: test_binary_sensors[binary_sensor.kitchen_lunar_ddeeff_timer_running-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'running',
|
||||
'friendly_name': 'LUNAR-DDEEFF Timer running',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'running',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Timer running',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.kitchen_lunar_ddeeff_timer_running',
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
# name: test_buttons[button.kitchen_lunar_ddeeff_reset_timer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LUNAR-DDEEFF Reset timer',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Reset timer',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.kitchen_lunar_ddeeff_reset_timer',
|
||||
@@ -89,7 +89,7 @@
|
||||
# name: test_buttons[button.kitchen_lunar_ddeeff_start_stop_timer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LUNAR-DDEEFF Start/stop timer',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Start/stop timer',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.kitchen_lunar_ddeeff_start_stop_timer',
|
||||
@@ -139,7 +139,7 @@
|
||||
# name: test_buttons[button.kitchen_lunar_ddeeff_tare-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LUNAR-DDEEFF Tare',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Tare',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.kitchen_lunar_ddeeff_tare',
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
# name: test_sensors[sensor.kitchen_lunar_ddeeff_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'LUNAR-DDEEFF Battery',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Battery',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.kitchen_lunar_ddeeff_battery',
|
||||
@@ -99,10 +99,10 @@
|
||||
# name: test_sensors[sensor.kitchen_lunar_ddeeff_volume_flow_rate-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_flow_rate',
|
||||
'friendly_name': 'LUNAR-DDEEFF Volume flow rate',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'volume_flow_rate',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Volume flow rate',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumeFlowRate.MILLILITERS_PER_SECOND: 'mL/s'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfVolumeFlowRate.MILLILITERS_PER_SECOND: 'mL/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.kitchen_lunar_ddeeff_volume_flow_rate',
|
||||
@@ -157,10 +157,10 @@
|
||||
# name: test_sensors[sensor.kitchen_lunar_ddeeff_weight-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'weight',
|
||||
'friendly_name': 'LUNAR-DDEEFF Weight',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'weight',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'LUNAR-DDEEFF Weight',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfMass.OUNCES: 'oz'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfMass.OUNCES: 'oz'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.kitchen_lunar_ddeeff_weight',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -457,15 +457,15 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<WeatherEntityStateAttribute.APPARENT_TEMPERATURE: 'apparent_temperature'>: 22.8,
|
||||
'attribution': 'Data provided by AccuWeather',
|
||||
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by AccuWeather',
|
||||
<WeatherEntityStateAttribute.CLOUD_COVERAGE: 'cloud_coverage'>: 10,
|
||||
<WeatherEntityStateAttribute.DEW_POINT: 'dew_point'>: 16.2,
|
||||
'friendly_name': 'Home',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home',
|
||||
<WeatherEntityStateAttribute.HUMIDITY: 'humidity'>: 67,
|
||||
<WeatherEntityStateAttribute.PRECIPITATION_UNIT: 'precipitation_unit'>: <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
<WeatherEntityStateAttribute.PRESSURE: 'pressure'>: 1012.0,
|
||||
<WeatherEntityStateAttribute.PRESSURE_UNIT: 'pressure_unit'>: <UnitOfPressure.HPA: 'hPa'>,
|
||||
'supported_features': <WeatherEntityFeature: 3>,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <WeatherEntityFeature: 3>,
|
||||
<WeatherEntityStateAttribute.TEMPERATURE: 'temperature'>: 22.6,
|
||||
<WeatherEntityStateAttribute.TEMPERATURE_UNIT: 'temperature_unit'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
<WeatherEntityStateAttribute.UV_INDEX: 'uv_index'>: 6,
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<ClimateEntityStateAttribute.CURRENT_HUMIDITY: 'current_humidity'>: 50.0,
|
||||
<ClimateEntityStateAttribute.CURRENT_TEMPERATURE: 'current_temperature'>: 22.0,
|
||||
'friendly_name': 'Living Room',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Room',
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
@@ -61,7 +61,7 @@
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 385>,
|
||||
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 24.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -137,7 +137,7 @@
|
||||
'medium',
|
||||
'high',
|
||||
]),
|
||||
'friendly_name': 'Test System',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test System',
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
@@ -147,7 +147,7 @@
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30.0,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16.0,
|
||||
'supported_features': <ClimateEntityFeature: 393>,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 393>,
|
||||
<ClimateEntityStateAttribute.TEMPERATURE: 'temperature'>: 24.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
# name: test_switch_entities[switch.test_system_away_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test System Away mode',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test System Away mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_system_away_mode',
|
||||
@@ -89,7 +89,7 @@
|
||||
# name: test_switch_entities[switch.test_system_continuous_fan-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test System Continuous fan',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test System Continuous fan',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_system_continuous_fan',
|
||||
@@ -139,7 +139,7 @@
|
||||
# name: test_switch_entities[switch.test_system_quiet_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test System Quiet mode',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test System Quiet mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_system_quiet_mode',
|
||||
@@ -189,7 +189,7 @@
|
||||
# name: test_switch_entities[switch.test_system_turbo_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test System Turbo mode',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test System Turbo mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_system_turbo_mode',
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
# name: test_fallback_to_get_rooms[sensor.room_1_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Room 1 Energy',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Energy',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_energy',
|
||||
@@ -105,10 +105,10 @@
|
||||
# name: test_fallback_to_get_rooms[sensor.room_1_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Room 1 Temperature',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_temperature',
|
||||
@@ -166,10 +166,10 @@
|
||||
# name: test_multiple_devices_create_individual_sensors[sensor.room_1_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Room 1 Energy',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Energy',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_energy',
|
||||
@@ -224,10 +224,10 @@
|
||||
# name: test_multiple_devices_create_individual_sensors[sensor.room_1_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Room 1 Temperature',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_temperature',
|
||||
@@ -285,10 +285,10 @@
|
||||
# name: test_multiple_devices_create_individual_sensors[sensor.room_2_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Room 2 Energy',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 2 Energy',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_2_energy',
|
||||
@@ -343,10 +343,10 @@
|
||||
# name: test_multiple_devices_create_individual_sensors[sensor.room_2_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Room 2 Temperature',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 2 Temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_2_temperature',
|
||||
@@ -404,10 +404,10 @@
|
||||
# name: test_sensor_cloud[sensor.room_1_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Room 1 Energy',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Energy',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_energy',
|
||||
@@ -462,10 +462,10 @@
|
||||
# name: test_sensor_cloud[sensor.room_1_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Room 1 Temperature',
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Room 1 Temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.room_1_temperature',
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_average_processing_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Average processing speed',
|
||||
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Average processing speed',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.MILLISECONDS: 'ms'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_average_processing_speed',
|
||||
@@ -90,8 +90,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_dns_queries-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home DNS queries',
|
||||
'unit_of_measurement': 'queries',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home DNS queries',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'queries',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_dns_queries',
|
||||
@@ -141,8 +141,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_dns_queries_blocked-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home DNS queries blocked',
|
||||
'unit_of_measurement': 'queries',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home DNS queries blocked',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'queries',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_dns_queries_blocked',
|
||||
@@ -192,8 +192,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_dns_queries_blocked_ratio-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home DNS queries blocked ratio',
|
||||
'unit_of_measurement': '%',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home DNS queries blocked ratio',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_dns_queries_blocked_ratio',
|
||||
@@ -243,8 +243,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_parental_control_blocked-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Parental control blocked',
|
||||
'unit_of_measurement': 'requests',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Parental control blocked',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'requests',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_parental_control_blocked',
|
||||
@@ -294,8 +294,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_rules_count-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Rules count',
|
||||
'unit_of_measurement': 'rules',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Rules count',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'rules',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_rules_count',
|
||||
@@ -345,8 +345,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_safe_browsing_blocked-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe browsing blocked',
|
||||
'unit_of_measurement': 'requests',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Safe browsing blocked',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'requests',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_safe_browsing_blocked',
|
||||
@@ -396,8 +396,8 @@
|
||||
# name: test_sensors[sensor.adguard_home_safe_searches_enforced-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe searches enforced',
|
||||
'unit_of_measurement': 'requests',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Safe searches enforced',
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'requests',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.adguard_home_safe_searches_enforced',
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
# name: test_switch[switch.adguard_home_filtering-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Filtering',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Filtering',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_filtering',
|
||||
@@ -89,7 +89,7 @@
|
||||
# name: test_switch[switch.adguard_home_parental_control-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Parental control',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Parental control',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_parental_control',
|
||||
@@ -139,7 +139,7 @@
|
||||
# name: test_switch[switch.adguard_home_protection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Protection',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Protection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_protection',
|
||||
@@ -189,7 +189,7 @@
|
||||
# name: test_switch[switch.adguard_home_query_log-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Query log',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Query log',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_query_log',
|
||||
@@ -239,7 +239,7 @@
|
||||
# name: test_switch[switch.adguard_home_safe_browsing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe browsing',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Safe browsing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_safe_browsing',
|
||||
@@ -289,7 +289,7 @@
|
||||
# name: test_switch[switch.adguard_home_safe_search-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe search',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home Safe search',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_safe_search',
|
||||
|
||||
@@ -41,15 +41,15 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<UpdateEntityStateAttribute.AUTO_UPDATE: 'auto_update'>: False,
|
||||
<UpdateEntityStateAttribute.DISPLAY_PRECISION: 'display_precision'>: 0,
|
||||
'entity_picture': '/api/brands/integration/adguard/icon.png',
|
||||
'friendly_name': 'AdGuard Home',
|
||||
<EntityStateAttribute.ENTITY_PICTURE: 'entity_picture'>: '/api/brands/integration/adguard/icon.png',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AdGuard Home',
|
||||
<UpdateEntityStateAttribute.IN_PROGRESS: 'in_progress'>: False,
|
||||
<UpdateEntityStateAttribute.INSTALLED_VERSION: 'installed_version'>: 'v0.107.50',
|
||||
<UpdateEntityStateAttribute.LATEST_VERSION: 'latest_version'>: 'v0.107.59',
|
||||
<UpdateEntityStateAttribute.RELEASE_SUMMARY: 'release_summary'>: 'AdGuard Home v0.107.59 is now available!',
|
||||
<UpdateEntityStateAttribute.RELEASE_URL: 'release_url'>: 'https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59',
|
||||
<UpdateEntityStateAttribute.SKIPPED_VERSION: 'skipped_version'>: None,
|
||||
'supported_features': <UpdateEntityFeature: 1>,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <UpdateEntityFeature: 1>,
|
||||
<UpdateEntityStateAttribute.TITLE: 'title'>: None,
|
||||
<UpdateEntityStateAttribute.UPDATE_PERCENTAGE: 'update_percentage'>: None,
|
||||
}),
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
'high',
|
||||
'auto',
|
||||
]),
|
||||
'friendly_name': 'myauto',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'myauto',
|
||||
<ClimateEntityStateAttribute.HVAC_ACTION: 'hvac_action'>: <HVACAction.COOLING: 'cooling'>,
|
||||
<ClimateEntityCapabilityAttribute.HVAC_MODES: 'hvac_modes'>: list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
@@ -47,7 +47,7 @@
|
||||
'MyTemp',
|
||||
'MyAuto',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 410>,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 410>,
|
||||
<ClimateEntityStateAttribute.TARGET_TEMP_HIGH: 'target_temp_high'>: 24,
|
||||
<ClimateEntityStateAttribute.TARGET_TEMP_LOW: 'target_temp_low'>: 20,
|
||||
<ClimateEntityCapabilityAttribute.TARGET_TEMP_STEP: 'target_temp_step'>: 1,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user