Compare commits

...

22 Commits

Author SHA1 Message Date
Paul Bottein 6860e0f3b9 Remove comment 2026-06-25 13:02:39 +02:00
Paul Bottein d3be0cc852 Simplify live attribute handling and use event enum 2026-06-25 12:49:01 +02:00
Paul Bottein c830c05cbd Expose selected state attributes in the logbook 2026-06-25 12:15:44 +02:00
Ronald van der Meer 2be55a06cc Migrate Duco sensor units to UnitOfRatio (#174791) 2026-06-25 10:09:55 +02:00
bkobus-bbx d786fb16a0 Migrate blebox to UnitOfDensity / UnitOfRatio enums (#174790) 2026-06-25 10:08:56 +02:00
J. Nick Koston f78dd797b1 Bump habluetooth to 6.25.1 (#174700) 2026-06-25 09:40:43 +02:00
davidrule1969 0d957a971d Bump pySwitchbot to 2.3.0 (#174678) 2026-06-25 08:51:22 +02:00
Raphael Hehl cff3a711f3 Bump uiprotect to 15.0.0 (#174709) 2026-06-25 08:42:32 +02:00
Brandon Rothweiler 177c4a4fb5 Bump dropbox to silver quality (#174706) 2026-06-25 07:39:05 +02:00
Samuel Xiao 7d8204f5e7 Bump switchbot-api to 2.12.0 (#174705) 2026-06-25 07:37:59 +02:00
Franck Nijhof 9aed167f71 Bump version to 2026.8.0.dev0 (#174693) 2026-06-25 00:44:19 +02:00
Franck Nijhof a8630f5570 Add delegated charging mode to Renault integration (#174687) 2026-06-24 23:35:13 +02:00
J. Nick Koston 2a75b0e2fb Bump habluetooth to 6.24.0 (#174688) 2026-06-24 23:34:38 +02:00
Brandon Rothweiler 9c4ad761c4 Add missing scope and authorize param to Dropbox OAuth (#174587) 2026-06-24 22:26:30 +02:00
Erwin Douna 8e3e1044a1 Tami4 group executor job (#174668) 2026-06-24 22:01:24 +02:00
Colin bec6c94e32 openevse: Convert config to textselector (#174675) 2026-06-24 21:50:41 +02:00
Colin c9729df69a openevse: Add missing callback test (#174560) 2026-06-24 21:33:30 +02:00
Christian Lackas 70ff0fd682 Bump homematicip to 2.13.2 (#174673) 2026-06-24 20:43:23 +02:00
Erwin Douna 258ae6d506 Vera core group executor job (#174669) 2026-06-24 20:37:08 +02:00
Ville Skyttä 4f93afd6ae Remove myself from huawei_lte codeowners (#174671) 2026-06-24 19:57:24 +02:00
Erwin Douna 7968fc4809 Huawei group executor job (#174666) 2026-06-24 19:43:54 +02:00
TheJulianJES 975f2a831e Bump zha-quirks to 2.1.0 (#174662) 2026-06-24 18:42:41 +02:00
43 changed files with 446 additions and 111 deletions
+1 -1
View File
@@ -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 -2
View File
@@ -790,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
+7 -8
View File
@@ -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"
]
}
+14 -1
View File
@@ -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%]"
}
+6 -7
View File
@@ -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,
@@ -52,7 +52,9 @@ __all__ = [
"DoorbellEventType",
"EventDeviceClass",
"EventEntity",
"EventEntityCapabilityAttribute",
"EventEntityDescription",
"EventEntityStateAttribute",
]
# mypy: disallow-any-generics
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.13.1"]
"requirements": ["homematicip==2.13.2"]
}
@@ -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",
@@ -1,6 +1,7 @@
"""Event parser and human readable log generator."""
from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED
from homeassistant.components.event import EventEntityStateAttribute
from homeassistant.components.script import EVENT_SCRIPT_STARTED
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY
@@ -39,8 +40,12 @@ LOGBOOK_ENTRY_SOURCE = "source"
LOGBOOK_ENTRY_MESSAGE = "message"
LOGBOOK_ENTRY_NAME = "name"
LOGBOOK_ENTRY_STATE = "state"
LOGBOOK_ENTRY_ATTRIBUTES = "attributes"
LOGBOOK_ENTRY_WHEN = "when"
# State attributes surfaced in logbook entries; extend as needed.
EXPOSED_STATE_ATTRIBUTES = {EventEntityStateAttribute.EVENT_TYPE}
# Automation events that can affect an entity_id or device_id
AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED}
+7 -3
View File
@@ -106,10 +106,11 @@ CONTEXT_PARENT_ID_BIN_POS: Final = 6
STATE_POS: Final = 7
ENTITY_ID_POS: Final = 8
ICON_POS: Final = 9
CONTEXT_ONLY_POS: Final = 10
ATTRIBUTES_POS: Final = 10
CONTEXT_ONLY_POS: Final = 11
# - For EventAsRow, additional fields are:
DATA_POS: Final = 11
CONTEXT_POS: Final = 12
DATA_POS: Final = 12
CONTEXT_POS: Final = 13
@final # Final to allow direct checking of the type instead of using isinstance
@@ -129,6 +130,7 @@ class EventAsRow(NamedTuple):
state: str | None
entity_id: str | None
icon: str | None
attributes: Mapping[str, Any] | None
context_only: bool | None
# Additional fields for EventAsRow
@@ -152,6 +154,7 @@ def async_event_to_row(event: Event) -> EventAsRow:
state=None,
entity_id=None,
icon=None,
attributes=None,
context_only=None,
data=event.data,
context=context,
@@ -175,6 +178,7 @@ def async_event_to_row(event: Event) -> EventAsRow:
state=new_state.state,
entity_id=new_state.entity_id,
icon=new_state.attributes.get(ATTR_ICON),
attributes=new_state.attributes,
context_only=None,
data=event.data,
context=context,
+27 -1
View File
@@ -1,6 +1,6 @@
"""Event parser and human readable log generator."""
from collections.abc import Callable, Collection, Generator, Sequence
from collections.abc import Callable, Collection, Generator, Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime as dt
import logging
@@ -16,6 +16,7 @@ from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.filters import Filters
from homeassistant.components.recorder.models import (
bytes_to_uuid_hex_or_none,
decode_attributes_from_source,
extract_event_type_ids,
extract_metadata_ids,
process_timestamp_to_utc_isoformat,
@@ -53,6 +54,8 @@ from .const import (
CONTEXT_STATE,
CONTEXT_USER_ID,
DOMAIN,
EXPOSED_STATE_ATTRIBUTES,
LOGBOOK_ENTRY_ATTRIBUTES,
LOGBOOK_ENTRY_DOMAIN,
LOGBOOK_ENTRY_ENTITY_ID,
LOGBOOK_ENTRY_ICON,
@@ -64,6 +67,7 @@ from .const import (
)
from .helpers import is_sensor_continuous
from .models import (
ATTRIBUTES_POS,
CONTEXT_ID_BIN_POS,
CONTEXT_ONLY_POS,
CONTEXT_PARENT_ID_BIN_POS,
@@ -273,6 +277,24 @@ class EventProcessor:
)
def _exposed_state_attributes(
row: Row | EventAsRow, attr_cache: dict[str, dict[str, Any]]
) -> dict[str, Any]:
"""Return the allowlisted state attributes for a state change row."""
attributes: Mapping[str, Any] | None
if type(row) is EventAsRow:
attributes = row[ATTRIBUTES_POS]
else:
attributes = decode_attributes_from_source(row[ATTRIBUTES_POS], attr_cache)
if not attributes:
return {}
return {
name: attributes[name]
for name in EXPOSED_STATE_ATTRIBUTES
if name in attributes
}
def _humanify(
hass: HomeAssistant,
rows: Generator[EventAsRow] | Sequence[Row] | Result,
@@ -294,6 +316,8 @@ def _humanify(
get_context = context_augmenter.get_context
context_id_bin: bytes
data: dict[str, Any]
# Decode each shared attribute set only once per run.
attr_cache: dict[str, dict[str, Any]] = {}
context_user_ids = logbook_run.context_user_ids
# Skip the LRU write on one-shot runs — the LogbookRun is discarded.
@@ -337,6 +361,8 @@ def _humanify(
data[LOGBOOK_ENTRY_NAME] = entity_name_cache_get(entity_id)
if icon := row[ICON_POS]:
data[LOGBOOK_ENTRY_ICON] = icon
if exposed := _exposed_state_attributes(row, attr_cache):
data[LOGBOOK_ENTRY_ATTRIBUTES] = exposed
elif event_type in external_events:
domain, describe_event = external_events[event_type]
@@ -14,6 +14,7 @@ from homeassistant.components.recorder.db_schema import (
EVENTS_CONTEXT_ID_BIN_INDEX,
OLD_FORMAT_ATTRS_JSON,
OLD_STATE,
SHARED_ATTR_OR_LEGACY_ATTRIBUTES,
SHARED_ATTRS_JSON,
SHARED_DATA_OR_LEGACY_EVENT_DATA,
STATES_CONTEXT_ID_BIN_INDEX,
@@ -65,12 +66,14 @@ STATE_COLUMNS = (
States.state.label("state"),
StatesMeta.entity_id.label("entity_id"),
ICON_OR_OLD_FORMAT_ICON_JSON,
SHARED_ATTR_OR_LEGACY_ATTRIBUTES,
)
STATE_CONTEXT_ONLY_COLUMNS = (
States.state.label("state"),
StatesMeta.entity_id.label("entity_id"),
literal(value=None, type_=sqlalchemy.String).label("icon"),
literal(value=None, type_=sqlalchemy.String).label("attributes"),
)
EVENT_COLUMNS_FOR_STATE_SELECT = (
@@ -93,6 +96,7 @@ EMPTY_STATE_COLUMNS = (
literal(value=None, type_=sqlalchemy.String).label("state"),
literal(value=None, type_=sqlalchemy.String).label("entity_id"),
literal(value=None, type_=sqlalchemy.String).label("icon"),
literal(value=None, type_=sqlalchemy.String).label("attributes"),
)
@@ -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"
)
),
}
)
@@ -9,6 +9,7 @@ from .context import (
from .database import DatabaseEngine, DatabaseOptimizer, UnsupportedDialect
from .event import extract_event_type_ids
from .state import LazyState, extract_metadata_ids, row_to_compressed_state
from .state_attributes import decode_attributes_from_source
from .statistics import (
CalendarStatisticPeriod,
FixedStatisticPeriod,
@@ -44,6 +45,7 @@ __all__ = [
"bytes_to_ulid_or_none",
"bytes_to_uuid_hex_or_none",
"datetime_to_timestamp_or_none",
"decode_attributes_from_source",
"extract_event_type_ids",
"extract_metadata_ids",
"process_timestamp",
@@ -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"
}
},
@@ -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"
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["uiprotect"],
"quality_scale": "platinum",
"requirements": ["uiprotect==14.0.0"]
"requirements": ["uiprotect==15.0.0"]
}
+7 -3
View File
@@ -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")
+1 -1
View File
@@ -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*",
+15
View File
@@ -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"
},
+1 -1
View File
@@ -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}"
+1 -1
View File
@@ -35,7 +35,7 @@ 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
+1 -1
View File
@@ -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."
+6 -6
View File
@@ -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
@@ -1219,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
@@ -1284,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
@@ -3114,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
@@ -3245,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
@@ -3453,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
@@ -128,6 +128,54 @@ async def test_already_configured(
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.parametrize(
("token", "expected_step"),
[
(
{
"access_token": "mock-access-token",
"expires_at": 9_999_999_999,
"scope": " ".join(OAUTH2_SCOPES),
},
"reauth_confirm",
),
(
{
"access_token": "mock-access-token",
"refresh_token": "mock-refresh-token",
"expires_at": 9_999_999_999,
"scope": "account_info.read files.content.read files.content.write",
},
"reauth_permissions",
),
],
ids=["missing_refresh_token", "missing_scope"],
)
async def test_reauth_confirm_step(
hass: HomeAssistant,
mock_config_entry,
token: dict[str, object],
expected_step: str,
) -> None:
"""Test reauth shows the correct confirmation step for the broken token."""
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, data={**mock_config_entry.data, "token": token}
)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == expected_step
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.EXTERNAL_STEP
assert result["step_id"] == "auth"
@pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.parametrize(
(
+43 -1
View File
@@ -1,11 +1,13 @@
"""Test the Dropbox integration setup."""
from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
from python_dropbox_api import DropboxAuthException, DropboxUnknownException
from homeassistant.config_entries import ConfigEntryState
from homeassistant.components.dropbox.const import DOMAIN, OAUTH2_SCOPES
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -80,6 +82,46 @@ async def test_setup_entry_implementation_unavailable(
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("mock_dropbox_client")
@pytest.mark.parametrize(
"token",
[
{
"access_token": "mock-access-token",
"expires_at": 9_999_999_999,
"scope": " ".join(OAUTH2_SCOPES),
},
{
"access_token": "mock-access-token",
"refresh_token": "mock-refresh-token",
"expires_at": 9_999_999_999,
"scope": "account_info.read files.content.read files.content.write",
},
],
ids=["missing_refresh_token", "missing_scope"],
)
async def test_setup_entry_triggers_reauth(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
token: dict[str, Any],
) -> None:
"""Test that a broken token triggers a reauth flow during setup."""
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, data={**mock_config_entry.data, "token": token}
)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
assert len(flows) == 1
assert flows[0]["context"]["source"] == SOURCE_REAUTH
assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id
@pytest.mark.usefixtures("mock_dropbox_client")
async def test_unload_entry(
hass: HomeAssistant,
@@ -35,7 +35,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_113_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.bathroom_rh_humidity-state]
@@ -44,7 +44,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bathroom RH Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bathroom_rh_humidity',
@@ -90,7 +90,7 @@
'supported_features': 0,
'translation_key': 'iaq_rh',
'unique_id': 'aa:bb:cc:dd:ee:ff_113_iaq_rh',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.bathroom_rh_humidity_air_quality_index-state]
@@ -98,7 +98,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bathroom RH Humidity air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bathroom_rh_humidity_air_quality_index',
@@ -144,7 +144,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_60_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-state]
@@ -153,7 +153,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_humidity',
@@ -199,7 +199,7 @@
'supported_features': 0,
'translation_key': 'iaq_rh',
'unique_id': 'aa:bb:cc:dd:ee:ff_60_iaq_rh',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-state]
@@ -207,7 +207,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Humidity air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_humidity_air_quality_index',
@@ -253,7 +253,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_61_co2',
'unit_of_measurement': 'ppm',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-state]
@@ -262,7 +262,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_carbon_dioxide',
@@ -308,7 +308,7 @@
'supported_features': 0,
'translation_key': 'iaq_co2',
'unique_id': 'aa:bb:cc:dd:ee:ff_61_iaq_co2',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-state]
@@ -316,7 +316,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve CO2 air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_co2_air_quality_index',
@@ -362,7 +362,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_50_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity-state]
@@ -371,7 +371,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Kitchen RH Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.kitchen_rh_humidity',
@@ -417,7 +417,7 @@
'supported_features': 0,
'translation_key': 'iaq_rh',
'unique_id': 'aa:bb:cc:dd:ee:ff_50_iaq_rh',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity_air_quality_index-state]
@@ -425,7 +425,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Kitchen RH Humidity air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.kitchen_rh_humidity_air_quality_index',
@@ -635,7 +635,7 @@
'supported_features': 0,
'translation_key': 'target_flow_level',
'unique_id': 'aa:bb:cc:dd:ee:ff_1_target_flow_level',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.living_target_flow_level-state]
@@ -643,7 +643,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Target flow level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.living_target_flow_level',
@@ -781,7 +781,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_2_co2',
'unit_of_measurement': 'ppm',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
})
# ---
# name: test_sensor_entities_state[sensor.office_co2_carbon_dioxide-state]
@@ -790,7 +790,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Office CO2 Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
}),
'context': <ANY>,
'entity_id': 'sensor.office_co2_carbon_dioxide',
@@ -836,7 +836,7 @@
'supported_features': 0,
'translation_key': 'iaq_co2',
'unique_id': 'aa:bb:cc:dd:ee:ff_2_iaq_co2',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.office_co2_co2_air_quality_index-state]
@@ -844,7 +844,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Office CO2 CO2 air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.office_co2_co2_air_quality_index',
@@ -890,7 +890,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_62_co2',
'unit_of_measurement': 'ppm',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-state]
@@ -899,7 +899,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_carbon_dioxide',
@@ -945,7 +945,7 @@
'supported_features': 0,
'translation_key': 'iaq_co2',
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_co2',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-state]
@@ -953,7 +953,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve CO2 air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_co2_air_quality_index',
@@ -999,7 +999,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_62_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_humidity-state]
@@ -1008,7 +1008,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_humidity',
@@ -1054,7 +1054,7 @@
'supported_features': 0,
'translation_key': 'iaq_rh',
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_rh',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-state]
@@ -1062,7 +1062,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Humidity air quality index',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_humidity_air_quality_index',
+43
View File
@@ -42,6 +42,7 @@ from homeassistant.const import (
EVENT_LOGBOOK_ENTRY,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import Context, Event, HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -345,6 +346,7 @@ def create_state_changed_event_from_old_new(
state=new_state and new_state.get("state"),
entity_id=entity_id,
icon=None,
attributes=None,
context_only=False,
data=None,
context=None,
@@ -1857,6 +1859,46 @@ async def test_icon_and_state(
assert response_json[2]["state"] == STATE_OFF
@pytest.mark.usefixtures("recorder_mock")
async def test_state_attributes_in_logbook(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test state attributes are exposed in the logbook like the history.
The recorded subset is surfaced, so attributes the recorder excludes
(such as supported_features) must not appear.
"""
await asyncio.gather(
*[
async_setup_component(hass, domain, {})
for domain in ("homeassistant", "logbook")
]
)
await async_recorder_block_till_done(hass)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
hass.states.async_set("event.doorbell", STATE_UNKNOWN, {"event_type": None})
hass.states.async_set(
"event.doorbell",
"2024-01-01T00:00:00.000+00:00",
{"event_type": "ring", "supported_features": 1},
)
hass.states.async_set(
"event.doorbell", "2024-01-01T00:01:00.000+00:00", {"event_type": "motion"}
)
await async_wait_recording_done(hass)
client = await hass_client()
response_json = await _async_fetch_logbook(client)
entries = [e for e in response_json if e.get("entity_id") == "event.doorbell"]
assert len(entries) == 2
assert entries[0]["attributes"] == {"event_type": "ring"}
assert entries[1]["attributes"] == {"event_type": "motion"}
@pytest.mark.usefixtures("recorder_mock")
async def test_fire_logbook_entries(
hass: HomeAssistant, hass_client: ClientSessionGenerator
@@ -3335,6 +3377,7 @@ async def test_parent_user_attribution_does_not_use_origin_event_fallback(
state=STATE_ON,
entity_id="switch.heater",
icon=None,
attributes=None,
context_only=False,
data={},
context=child_context,
+1
View File
@@ -19,6 +19,7 @@ def test_lazy_event_partial_state_context() -> None:
state="state",
entity_id="entity_id",
icon="icon",
attributes=None,
context_only=False,
data={},
context=Mock(),
@@ -35,6 +35,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -1479,6 +1480,64 @@ async def test_subscribe_unsubscribe_logbook_stream(
) == listeners_without_writes(init_listeners)
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
async def test_subscribe_logbook_stream_state_attributes(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test the live logbook stream exposes allowlisted state attributes."""
now = dt_util.utcnow()
await asyncio.gather(
*[
async_setup_component(hass, domain, {})
for domain in ("homeassistant", "logbook")
]
)
await hass.async_block_till_done()
hass.states.async_set("binary_sensor.is_light", STATE_ON)
hass.states.async_set("binary_sensor.is_light", STATE_OFF)
await hass.async_block_till_done()
await async_wait_recording_done(hass)
websocket_client = await hass_ws_client()
await websocket_client.send_json(
{"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()}
)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == TYPE_RESULT
assert msg["success"]
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["event"]["partial"] is True
await hass.async_block_till_done()
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert "partial" not in msg["event"]
assert msg["event"]["events"] == []
hass.states.async_set("event.doorbell", STATE_UNKNOWN, {"event_type": None})
hass.states.async_set(
"event.doorbell",
"2024-01-01T00:00:00.000+00:00",
{"event_type": "ring", "supported_features": 1},
)
await hass.async_block_till_done()
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert msg["event"]["events"] == [
{
"entity_id": "event.doorbell",
"state": "2024-01-01T00:00:00.000+00:00",
"attributes": {"event_type": "ring"},
"when": ANY,
}
]
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
async def test_subscribe_unsubscribe_logbook_stream_entities(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
+23
View File
@@ -80,6 +80,29 @@ async def test_missing_sensor_graceful_handling(
assert state.state == "Charging"
async def test_websocket_callback_updates_entities(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_charger: MagicMock,
) -> None:
"""Test the websocket callback pushes updates to entity state."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state
assert state.state == "Charging"
mock_charger.status = "Sleeping"
await mock_charger.callback()
await hass.async_block_till_done()
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state
assert state.state == "Sleeping"
async def test_sensor_unavailable_on_coordinator_timeout(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -318,6 +318,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -359,6 +360,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -1143,6 +1145,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -1184,6 +1187,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -2411,6 +2415,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -2452,6 +2457,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -3400,6 +3406,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -3441,6 +3448,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -4273,6 +4281,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -4314,6 +4323,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -5088,6 +5098,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -5129,6 +5140,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -5971,6 +5983,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),
@@ -6012,6 +6025,7 @@
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'always',
'delayed',
'delegated',
'scheduled',
]),
}),