mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
2022.10.1 (#79751)
This commit is contained in:
commit
aabd681d7e
@ -162,7 +162,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/brunt/ @eavanvalkenburg
|
/tests/components/brunt/ @eavanvalkenburg
|
||||||
/homeassistant/components/bsblan/ @liudger
|
/homeassistant/components/bsblan/ @liudger
|
||||||
/tests/components/bsblan/ @liudger
|
/tests/components/bsblan/ @liudger
|
||||||
/homeassistant/components/bt_smarthub/ @jxwolstenholme
|
/homeassistant/components/bt_smarthub/ @typhoon2099
|
||||||
/homeassistant/components/bthome/ @Ernst79
|
/homeassistant/components/bthome/ @Ernst79
|
||||||
/tests/components/bthome/ @Ernst79
|
/tests/components/bthome/ @Ernst79
|
||||||
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
|
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
|
||||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from dataclasses import asdict
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
@ -185,11 +184,11 @@ class BluetoothManager:
|
|||||||
"adapters": self._adapters,
|
"adapters": self._adapters,
|
||||||
"scanners": scanner_diagnostics,
|
"scanners": scanner_diagnostics,
|
||||||
"connectable_history": [
|
"connectable_history": [
|
||||||
asdict(service_info)
|
service_info.as_dict()
|
||||||
for service_info in self._connectable_history.values()
|
for service_info in self._connectable_history.values()
|
||||||
],
|
],
|
||||||
"history": [
|
"history": [
|
||||||
asdict(service_info) for service_info in self._history.values()
|
service_info.as_dict() for service_info in self._history.values()
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,25 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo):
|
|||||||
connectable: bool
|
connectable: bool
|
||||||
time: float
|
time: float
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return as dict.
|
||||||
|
|
||||||
|
The dataclass asdict method is not used because
|
||||||
|
it will try to deepcopy pyobjc data which will fail.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"address": self.address,
|
||||||
|
"rssi": self.rssi,
|
||||||
|
"manufacturer_data": self.manufacturer_data,
|
||||||
|
"service_data": self.service_data,
|
||||||
|
"service_uuids": self.service_uuids,
|
||||||
|
"source": self.source,
|
||||||
|
"advertisement": self.advertisement,
|
||||||
|
"connectable": self.connectable,
|
||||||
|
"time": self.time,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BluetoothScanningMode(Enum):
|
class BluetoothScanningMode(Enum):
|
||||||
"""The mode of scanning for bluetooth devices."""
|
"""The mode of scanning for bluetooth devices."""
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
"domain": "bt_smarthub",
|
"domain": "bt_smarthub",
|
||||||
"name": "BT Smart Hub",
|
"name": "BT Smart Hub",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bt_smarthub",
|
"documentation": "https://www.home-assistant.io/integrations/bt_smarthub",
|
||||||
"requirements": ["btsmarthub_devicelist==0.2.2"],
|
"requirements": ["btsmarthub_devicelist==0.2.3"],
|
||||||
"codeowners": ["@jxwolstenholme"],
|
"codeowners": ["@typhoon2099"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["btsmarthub_devicelist"]
|
"loggers": ["btsmarthub_devicelist"]
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": ["home-assistant-frontend==20221005.0"],
|
"requirements": ["home-assistant-frontend==20221006.0"],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
"auth",
|
"auth",
|
||||||
|
@ -23,22 +23,11 @@ from homeassistant.components.recorder.models import (
|
|||||||
StatisticMetaData,
|
StatisticMetaData,
|
||||||
StatisticResult,
|
StatisticResult,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT
|
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import entity_sources
|
from homeassistant.helpers.entity import entity_sources
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.unit_conversion import (
|
|
||||||
BaseUnitConverter,
|
|
||||||
DistanceConverter,
|
|
||||||
EnergyConverter,
|
|
||||||
MassConverter,
|
|
||||||
PowerConverter,
|
|
||||||
PressureConverter,
|
|
||||||
SpeedConverter,
|
|
||||||
TemperatureConverter,
|
|
||||||
VolumeConverter,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
ATTR_LAST_RESET,
|
ATTR_LAST_RESET,
|
||||||
@ -48,7 +37,6 @@ from . import (
|
|||||||
STATE_CLASS_TOTAL,
|
STATE_CLASS_TOTAL,
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
STATE_CLASSES,
|
STATE_CLASSES,
|
||||||
SensorDeviceClass,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -59,18 +47,6 @@ DEFAULT_STATISTICS = {
|
|||||||
STATE_CLASS_TOTAL_INCREASING: {"sum"},
|
STATE_CLASS_TOTAL_INCREASING: {"sum"},
|
||||||
}
|
}
|
||||||
|
|
||||||
UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
|
|
||||||
SensorDeviceClass.DISTANCE: DistanceConverter,
|
|
||||||
SensorDeviceClass.ENERGY: EnergyConverter,
|
|
||||||
SensorDeviceClass.GAS: VolumeConverter,
|
|
||||||
SensorDeviceClass.POWER: PowerConverter,
|
|
||||||
SensorDeviceClass.PRESSURE: PressureConverter,
|
|
||||||
SensorDeviceClass.SPEED: SpeedConverter,
|
|
||||||
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
|
|
||||||
SensorDeviceClass.VOLUME: VolumeConverter,
|
|
||||||
SensorDeviceClass.WEIGHT: MassConverter,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep track of entities for which a warning about decreasing value has been logged
|
# Keep track of entities for which a warning about decreasing value has been logged
|
||||||
SEEN_DIP = "sensor_seen_total_increasing_dip"
|
SEEN_DIP = "sensor_seen_total_increasing_dip"
|
||||||
WARN_DIP = "sensor_warn_total_increasing_dip"
|
WARN_DIP = "sensor_warn_total_increasing_dip"
|
||||||
@ -154,84 +130,91 @@ def _normalize_states(
|
|||||||
session: Session,
|
session: Session,
|
||||||
old_metadatas: dict[str, tuple[int, StatisticMetaData]],
|
old_metadatas: dict[str, tuple[int, StatisticMetaData]],
|
||||||
entity_history: Iterable[State],
|
entity_history: Iterable[State],
|
||||||
device_class: str | None,
|
|
||||||
entity_id: str,
|
entity_id: str,
|
||||||
) -> tuple[str | None, str | None, list[tuple[float, State]]]:
|
) -> tuple[str | None, str | None, list[tuple[float, State]]]:
|
||||||
"""Normalize units."""
|
"""Normalize units."""
|
||||||
old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None
|
old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None
|
||||||
state_unit: str | None = None
|
state_unit: str | None = None
|
||||||
|
|
||||||
if device_class not in UNIT_CONVERTERS or (
|
fstates: list[tuple[float, State]] = []
|
||||||
old_metadata
|
|
||||||
and old_metadata["unit_of_measurement"]
|
|
||||||
not in UNIT_CONVERTERS[device_class].VALID_UNITS
|
|
||||||
):
|
|
||||||
# We're either not normalizing this device class or this entity is not stored
|
|
||||||
# in a supported unit, return the states as they are
|
|
||||||
fstates = []
|
|
||||||
for state in entity_history:
|
|
||||||
try:
|
|
||||||
fstate = _parse_float(state.state)
|
|
||||||
except (ValueError, TypeError): # TypeError to guard for NULL state in DB
|
|
||||||
continue
|
|
||||||
fstates.append((fstate, state))
|
|
||||||
|
|
||||||
if fstates:
|
|
||||||
all_units = _get_units(fstates)
|
|
||||||
if len(all_units) > 1:
|
|
||||||
if WARN_UNSTABLE_UNIT not in hass.data:
|
|
||||||
hass.data[WARN_UNSTABLE_UNIT] = set()
|
|
||||||
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
|
||||||
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
|
||||||
extra = ""
|
|
||||||
if old_metadata:
|
|
||||||
extra = (
|
|
||||||
" and matches the unit of already compiled statistics "
|
|
||||||
f"({old_metadata['unit_of_measurement']})"
|
|
||||||
)
|
|
||||||
_LOGGER.warning(
|
|
||||||
"The unit of %s is changing, got multiple %s, generation of long term "
|
|
||||||
"statistics will be suppressed unless the unit is stable%s. "
|
|
||||||
"Go to %s to fix this",
|
|
||||||
entity_id,
|
|
||||||
all_units,
|
|
||||||
extra,
|
|
||||||
LINK_DEV_STATISTICS,
|
|
||||||
)
|
|
||||||
return None, None, []
|
|
||||||
state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
|
||||||
return state_unit, state_unit, fstates
|
|
||||||
|
|
||||||
converter = UNIT_CONVERTERS[device_class]
|
|
||||||
fstates = []
|
|
||||||
|
|
||||||
statistics_unit: str | None = None
|
|
||||||
if old_metadata:
|
|
||||||
statistics_unit = old_metadata["unit_of_measurement"]
|
|
||||||
|
|
||||||
for state in entity_history:
|
for state in entity_history:
|
||||||
try:
|
try:
|
||||||
fstate = _parse_float(state.state)
|
fstate = _parse_float(state.state)
|
||||||
except ValueError:
|
except (ValueError, TypeError): # TypeError to guard for NULL state in DB
|
||||||
continue
|
continue
|
||||||
|
fstates.append((fstate, state))
|
||||||
|
|
||||||
|
if not fstates:
|
||||||
|
return None, None, fstates
|
||||||
|
|
||||||
|
state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
|
statistics_unit: str | None
|
||||||
|
if not old_metadata:
|
||||||
|
# We've not seen this sensor before, the first valid state determines the unit
|
||||||
|
# used for statistics
|
||||||
|
statistics_unit = state_unit
|
||||||
|
else:
|
||||||
|
# We have seen this sensor before, use the unit from metadata
|
||||||
|
statistics_unit = old_metadata["unit_of_measurement"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
not statistics_unit
|
||||||
|
or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER
|
||||||
|
):
|
||||||
|
# The unit used by this sensor doesn't support unit conversion
|
||||||
|
|
||||||
|
all_units = _get_units(fstates)
|
||||||
|
if len(all_units) > 1:
|
||||||
|
if WARN_UNSTABLE_UNIT not in hass.data:
|
||||||
|
hass.data[WARN_UNSTABLE_UNIT] = set()
|
||||||
|
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
||||||
|
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
||||||
|
extra = ""
|
||||||
|
if old_metadata:
|
||||||
|
extra = (
|
||||||
|
" and matches the unit of already compiled statistics "
|
||||||
|
f"({old_metadata['unit_of_measurement']})"
|
||||||
|
)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The unit of %s is changing, got multiple %s, generation of long term "
|
||||||
|
"statistics will be suppressed unless the unit is stable%s. "
|
||||||
|
"Go to %s to fix this",
|
||||||
|
entity_id,
|
||||||
|
all_units,
|
||||||
|
extra,
|
||||||
|
LINK_DEV_STATISTICS,
|
||||||
|
)
|
||||||
|
return None, None, []
|
||||||
|
state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
return state_unit, state_unit, fstates
|
||||||
|
|
||||||
|
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit]
|
||||||
|
valid_fstates: list[tuple[float, State]] = []
|
||||||
|
|
||||||
|
for fstate, state in fstates:
|
||||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
# Exclude unsupported units from statistics
|
# Exclude states with unsupported unit from statistics
|
||||||
if state_unit not in converter.VALID_UNITS:
|
if state_unit not in converter.VALID_UNITS:
|
||||||
if WARN_UNSUPPORTED_UNIT not in hass.data:
|
if WARN_UNSUPPORTED_UNIT not in hass.data:
|
||||||
hass.data[WARN_UNSUPPORTED_UNIT] = set()
|
hass.data[WARN_UNSUPPORTED_UNIT] = set()
|
||||||
if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]:
|
if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]:
|
||||||
hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id)
|
hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id)
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"%s has unit %s which is unsupported for device_class %s",
|
"The unit of %s (%s) can not be converted to the unit of previously "
|
||||||
|
"compiled statistics (%s). Generation of long term statistics "
|
||||||
|
"will be suppressed unless the unit changes back to %s or a "
|
||||||
|
"compatible unit. "
|
||||||
|
"Go to %s to fix this",
|
||||||
entity_id,
|
entity_id,
|
||||||
state_unit,
|
state_unit,
|
||||||
device_class,
|
statistics_unit,
|
||||||
|
statistics_unit,
|
||||||
|
LINK_DEV_STATISTICS,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if statistics_unit is None:
|
|
||||||
statistics_unit = state_unit
|
|
||||||
|
|
||||||
fstates.append(
|
valid_fstates.append(
|
||||||
(
|
(
|
||||||
converter.convert(
|
converter.convert(
|
||||||
fstate, from_unit=state_unit, to_unit=statistics_unit
|
fstate, from_unit=state_unit, to_unit=statistics_unit
|
||||||
@ -240,7 +223,7 @@ def _normalize_states(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return statistics_unit, state_unit, fstates
|
return statistics_unit, state_unit, valid_fstates
|
||||||
|
|
||||||
|
|
||||||
def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str:
|
def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str:
|
||||||
@ -427,14 +410,12 @@ def _compile_statistics( # noqa: C901
|
|||||||
if entity_id not in history_list:
|
if entity_id not in history_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
device_class = _state.attributes.get(ATTR_DEVICE_CLASS)
|
|
||||||
entity_history = history_list[entity_id]
|
entity_history = history_list[entity_id]
|
||||||
statistics_unit, state_unit, fstates = _normalize_states(
|
statistics_unit, state_unit, fstates = _normalize_states(
|
||||||
hass,
|
hass,
|
||||||
session,
|
session,
|
||||||
old_metadatas,
|
old_metadatas,
|
||||||
entity_history,
|
entity_history,
|
||||||
device_class,
|
|
||||||
entity_id,
|
entity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -467,11 +448,11 @@ def _compile_statistics( # noqa: C901
|
|||||||
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
if entity_id not in hass.data[WARN_UNSTABLE_UNIT]:
|
||||||
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
hass.data[WARN_UNSTABLE_UNIT].add(entity_id)
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The %sunit of %s (%s) does not match the unit of already "
|
"The unit of %s (%s) can not be converted to the unit of previously "
|
||||||
"compiled statistics (%s). Generation of long term statistics "
|
"compiled statistics (%s). Generation of long term statistics "
|
||||||
"will be suppressed unless the unit changes back to %s. "
|
"will be suppressed unless the unit changes back to %s or a "
|
||||||
|
"compatible unit. "
|
||||||
"Go to %s to fix this",
|
"Go to %s to fix this",
|
||||||
"normalized " if device_class in UNIT_CONVERTERS else "",
|
|
||||||
entity_id,
|
entity_id,
|
||||||
statistics_unit,
|
statistics_unit,
|
||||||
old_metadata[1]["unit_of_measurement"],
|
old_metadata[1]["unit_of_measurement"],
|
||||||
@ -603,7 +584,6 @@ def list_statistic_ids(
|
|||||||
|
|
||||||
for state in entities:
|
for state in entities:
|
||||||
state_class = state.attributes[ATTR_STATE_CLASS]
|
state_class = state.attributes[ATTR_STATE_CLASS]
|
||||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
|
||||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
provided_statistics = DEFAULT_STATISTICS[state_class]
|
provided_statistics = DEFAULT_STATISTICS[state_class]
|
||||||
@ -620,21 +600,6 @@ def list_statistic_ids(
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if device_class not in UNIT_CONVERTERS:
|
|
||||||
result[state.entity_id] = {
|
|
||||||
"has_mean": "mean" in provided_statistics,
|
|
||||||
"has_sum": "sum" in provided_statistics,
|
|
||||||
"name": None,
|
|
||||||
"source": RECORDER_DOMAIN,
|
|
||||||
"statistic_id": state.entity_id,
|
|
||||||
"unit_of_measurement": state_unit,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
|
|
||||||
converter = UNIT_CONVERTERS[device_class]
|
|
||||||
if state_unit not in converter.VALID_UNITS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
result[state.entity_id] = {
|
result[state.entity_id] = {
|
||||||
"has_mean": "mean" in provided_statistics,
|
"has_mean": "mean" in provided_statistics,
|
||||||
"has_sum": "sum" in provided_statistics,
|
"has_sum": "sum" in provided_statistics,
|
||||||
@ -643,6 +608,7 @@ def list_statistic_ids(
|
|||||||
"statistic_id": state.entity_id,
|
"statistic_id": state.entity_id,
|
||||||
"unit_of_measurement": state_unit,
|
"unit_of_measurement": state_unit,
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -660,7 +626,6 @@ def validate_statistics(
|
|||||||
|
|
||||||
for state in sensor_states:
|
for state in sensor_states:
|
||||||
entity_id = state.entity_id
|
entity_id = state.entity_id
|
||||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
|
||||||
state_class = state.attributes.get(ATTR_STATE_CLASS)
|
state_class = state.attributes.get(ATTR_STATE_CLASS)
|
||||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
@ -684,35 +649,30 @@ def validate_statistics(
|
|||||||
)
|
)
|
||||||
|
|
||||||
metadata_unit = metadata[1]["unit_of_measurement"]
|
metadata_unit = metadata[1]["unit_of_measurement"]
|
||||||
if device_class not in UNIT_CONVERTERS:
|
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
|
||||||
|
if not converter:
|
||||||
if state_unit != metadata_unit:
|
if state_unit != metadata_unit:
|
||||||
# The unit has changed
|
# The unit has changed, and it's not possible to convert
|
||||||
issue_type = (
|
|
||||||
"units_changed_can_convert"
|
|
||||||
if statistics.can_convert_units(metadata_unit, state_unit)
|
|
||||||
else "units_changed"
|
|
||||||
)
|
|
||||||
validation_result[entity_id].append(
|
validation_result[entity_id].append(
|
||||||
statistics.ValidationIssue(
|
statistics.ValidationIssue(
|
||||||
issue_type,
|
"units_changed",
|
||||||
{
|
{
|
||||||
"statistic_id": entity_id,
|
"statistic_id": entity_id,
|
||||||
"state_unit": state_unit,
|
"state_unit": state_unit,
|
||||||
"metadata_unit": metadata_unit,
|
"metadata_unit": metadata_unit,
|
||||||
|
"supported_unit": metadata_unit,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS:
|
elif state_unit not in converter.VALID_UNITS:
|
||||||
# The unit in metadata is not supported for this device class
|
# The state unit can't be converted to the unit in metadata
|
||||||
valid_units = ", ".join(
|
valid_units = ", ".join(sorted(converter.VALID_UNITS))
|
||||||
sorted(UNIT_CONVERTERS[device_class].VALID_UNITS)
|
|
||||||
)
|
|
||||||
validation_result[entity_id].append(
|
validation_result[entity_id].append(
|
||||||
statistics.ValidationIssue(
|
statistics.ValidationIssue(
|
||||||
"unsupported_unit_metadata",
|
"units_changed",
|
||||||
{
|
{
|
||||||
"statistic_id": entity_id,
|
"statistic_id": entity_id,
|
||||||
"device_class": device_class,
|
"state_unit": state_unit,
|
||||||
"metadata_unit": metadata_unit,
|
"metadata_unit": metadata_unit,
|
||||||
"supported_unit": valid_units,
|
"supported_unit": valid_units,
|
||||||
},
|
},
|
||||||
@ -728,23 +688,6 @@ def validate_statistics(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
state_class in STATE_CLASSES
|
|
||||||
and device_class in UNIT_CONVERTERS
|
|
||||||
and state_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS
|
|
||||||
):
|
|
||||||
# The unit in the state is not supported for this device class
|
|
||||||
validation_result[entity_id].append(
|
|
||||||
statistics.ValidationIssue(
|
|
||||||
"unsupported_unit_state",
|
|
||||||
{
|
|
||||||
"statistic_id": entity_id,
|
|
||||||
"device_class": device_class,
|
|
||||||
"state_unit": state_unit,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for statistic_id in sensor_statistic_ids - sensor_entity_ids:
|
for statistic_id in sensor_statistic_ids - sensor_entity_ids:
|
||||||
# There is no sensor matching the statistics_id
|
# There is no sensor matching the statistics_id
|
||||||
validation_result[statistic_id].append(
|
validation_result[statistic_id].append(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Config flow for ZHA."""
|
"""Config flow for ZHA."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
@ -65,8 +66,16 @@ FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup"
|
|||||||
CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup"
|
CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup"
|
||||||
OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee"
|
OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee"
|
||||||
|
|
||||||
|
OPTIONS_INTENT_MIGRATE = "intent_migrate"
|
||||||
|
OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure"
|
||||||
|
|
||||||
UPLOADED_BACKUP_FILE = "uploaded_backup_file"
|
UPLOADED_BACKUP_FILE = "uploaded_backup_file"
|
||||||
|
|
||||||
|
DEFAULT_ZHA_ZEROCONF_PORT = 6638
|
||||||
|
ESPHOME_API_PORT = 6053
|
||||||
|
|
||||||
|
CONNECT_DELAY_S = 1.0
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -159,6 +168,7 @@ class BaseZhaFlow(FlowHandler):
|
|||||||
yield app
|
yield app
|
||||||
finally:
|
finally:
|
||||||
await app.disconnect()
|
await app.disconnect()
|
||||||
|
await asyncio.sleep(CONNECT_DELAY_S)
|
||||||
|
|
||||||
async def _restore_backup(
|
async def _restore_backup(
|
||||||
self, backup: zigpy.backups.NetworkBackup, **kwargs: Any
|
self, backup: zigpy.backups.NetworkBackup, **kwargs: Any
|
||||||
@ -628,14 +638,21 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
|||||||
|
|
||||||
# Hostname is format: livingroom.local.
|
# Hostname is format: livingroom.local.
|
||||||
local_name = discovery_info.hostname[:-1]
|
local_name = discovery_info.hostname[:-1]
|
||||||
radio_type = discovery_info.properties.get("radio_type") or local_name
|
port = discovery_info.port or DEFAULT_ZHA_ZEROCONF_PORT
|
||||||
|
|
||||||
|
# Fix incorrect port for older TubesZB devices
|
||||||
|
if "tube" in local_name and port == ESPHOME_API_PORT:
|
||||||
|
port = DEFAULT_ZHA_ZEROCONF_PORT
|
||||||
|
|
||||||
|
if "radio_type" in discovery_info.properties:
|
||||||
|
self._radio_type = RadioType[discovery_info.properties["radio_type"]]
|
||||||
|
elif "efr32" in local_name:
|
||||||
|
self._radio_type = RadioType.ezsp
|
||||||
|
else:
|
||||||
|
self._radio_type = RadioType.znp
|
||||||
|
|
||||||
node_name = local_name[: -len(".local")]
|
node_name = local_name[: -len(".local")]
|
||||||
host = discovery_info.host
|
device_path = f"socket://{discovery_info.host}:{port}"
|
||||||
port = discovery_info.port
|
|
||||||
if local_name.startswith("tube") or "efr32" in local_name:
|
|
||||||
# This is hard coded to work with legacy devices
|
|
||||||
port = 6638
|
|
||||||
device_path = f"socket://{host}:{port}"
|
|
||||||
|
|
||||||
if current_entry := await self.async_set_unique_id(node_name):
|
if current_entry := await self.async_set_unique_id(node_name):
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
@ -651,13 +668,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
|||||||
self._title = device_path
|
self._title = device_path
|
||||||
self._device_path = device_path
|
self._device_path = device_path
|
||||||
|
|
||||||
if "efr32" in radio_type:
|
|
||||||
self._radio_type = RadioType.ezsp
|
|
||||||
elif "zigate" in radio_type:
|
|
||||||
self._radio_type = RadioType.zigate
|
|
||||||
else:
|
|
||||||
self._radio_type = RadioType.znp
|
|
||||||
|
|
||||||
return await self.async_step_confirm()
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
async def async_step_hardware(
|
async def async_step_hardware(
|
||||||
@ -720,10 +730,54 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow):
|
|||||||
# ZHA is not running
|
# ZHA is not running
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return await self.async_step_choose_serial_port()
|
return await self.async_step_prompt_migrate_or_reconfigure()
|
||||||
|
|
||||||
return self.async_show_form(step_id="init")
|
return self.async_show_form(step_id="init")
|
||||||
|
|
||||||
|
async def async_step_prompt_migrate_or_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Confirm if we are migrating adapters or just re-configuring."""
|
||||||
|
|
||||||
|
return self.async_show_menu(
|
||||||
|
step_id="prompt_migrate_or_reconfigure",
|
||||||
|
menu_options=[
|
||||||
|
OPTIONS_INTENT_RECONFIGURE,
|
||||||
|
OPTIONS_INTENT_MIGRATE,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_intent_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Virtual step for when the user is reconfiguring the integration."""
|
||||||
|
return await self.async_step_choose_serial_port()
|
||||||
|
|
||||||
|
async def async_step_intent_migrate(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Confirm the user wants to reset their current radio."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Reset the current adapter
|
||||||
|
async with self._connect_zigpy_app() as app:
|
||||||
|
await app.reset_network_info()
|
||||||
|
|
||||||
|
return await self.async_step_instruct_unplug()
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="intent_migrate")
|
||||||
|
|
||||||
|
async def async_step_instruct_unplug(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Instruct the user to unplug the current radio, if possible."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Now that the old radio is gone, we can scan for serial ports again
|
||||||
|
return await self.async_step_choose_serial_port()
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="instruct_unplug")
|
||||||
|
|
||||||
async def _async_create_radio_entity(self):
|
async def _async_create_radio_entity(self):
|
||||||
"""Re-implementation of the base flow's final step to update the config."""
|
"""Re-implementation of the base flow's final step to update the config."""
|
||||||
device_settings = self._device_settings.copy()
|
device_settings = self._device_settings.copy()
|
||||||
|
@ -76,6 +76,22 @@
|
|||||||
"title": "Reconfigure ZHA",
|
"title": "Reconfigure ZHA",
|
||||||
"description": "ZHA will be stopped. Do you wish to continue?"
|
"description": "ZHA will be stopped. Do you wish to continue?"
|
||||||
},
|
},
|
||||||
|
"prompt_migrate_or_reconfigure": {
|
||||||
|
"title": "Migrate or re-configure",
|
||||||
|
"description": "Are you migrating to a new radio or re-configuring the current radio?",
|
||||||
|
"menu_options": {
|
||||||
|
"intent_migrate": "Migrate to a new radio",
|
||||||
|
"intent_reconfigure": "Re-configure the current radio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"intent_migrate": {
|
||||||
|
"title": "Migrate to a new radio",
|
||||||
|
"description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?"
|
||||||
|
},
|
||||||
|
"instruct_unplug": {
|
||||||
|
"title": "Unplug your old radio",
|
||||||
|
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it."
|
||||||
|
},
|
||||||
"choose_serial_port": {
|
"choose_serial_port": {
|
||||||
"title": "[%key:component::zha::config::step::choose_serial_port::title%]",
|
"title": "[%key:component::zha::config::step::choose_serial_port::title%]",
|
||||||
"data": {
|
"data": {
|
||||||
|
@ -64,35 +64,12 @@
|
|||||||
"description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.",
|
"description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.",
|
||||||
"title": "Overwrite Radio IEEE Address"
|
"title": "Overwrite Radio IEEE Address"
|
||||||
},
|
},
|
||||||
"pick_radio": {
|
|
||||||
"data": {
|
|
||||||
"radio_type": "Radio Type"
|
|
||||||
},
|
|
||||||
"description": "Pick a type of your Zigbee radio",
|
|
||||||
"title": "Radio Type"
|
|
||||||
},
|
|
||||||
"port_config": {
|
|
||||||
"data": {
|
|
||||||
"baudrate": "port speed",
|
|
||||||
"flow_control": "data flow control",
|
|
||||||
"path": "Serial device path"
|
|
||||||
},
|
|
||||||
"description": "Enter port specific settings",
|
|
||||||
"title": "Settings"
|
|
||||||
},
|
|
||||||
"upload_manual_backup": {
|
"upload_manual_backup": {
|
||||||
"data": {
|
"data": {
|
||||||
"uploaded_backup_file": "Upload a file"
|
"uploaded_backup_file": "Upload a file"
|
||||||
},
|
},
|
||||||
"description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.",
|
"description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.",
|
||||||
"title": "Upload a Manual Backup"
|
"title": "Upload a Manual Backup"
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"path": "Serial Device Path"
|
|
||||||
},
|
|
||||||
"description": "Select serial port for Zigbee radio",
|
|
||||||
"title": "ZHA"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -212,6 +189,14 @@
|
|||||||
"description": "ZHA will be stopped. Do you wish to continue?",
|
"description": "ZHA will be stopped. Do you wish to continue?",
|
||||||
"title": "Reconfigure ZHA"
|
"title": "Reconfigure ZHA"
|
||||||
},
|
},
|
||||||
|
"instruct_unplug": {
|
||||||
|
"description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.",
|
||||||
|
"title": "Unplug your old radio"
|
||||||
|
},
|
||||||
|
"intent_migrate": {
|
||||||
|
"description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?",
|
||||||
|
"title": "Migrate to a new radio"
|
||||||
|
},
|
||||||
"manual_pick_radio_type": {
|
"manual_pick_radio_type": {
|
||||||
"data": {
|
"data": {
|
||||||
"radio_type": "Radio Type"
|
"radio_type": "Radio Type"
|
||||||
@ -235,6 +220,14 @@
|
|||||||
"description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.",
|
"description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.",
|
||||||
"title": "Overwrite Radio IEEE Address"
|
"title": "Overwrite Radio IEEE Address"
|
||||||
},
|
},
|
||||||
|
"prompt_migrate_or_reconfigure": {
|
||||||
|
"description": "Are you migrating to a new radio or re-configuring the current radio?",
|
||||||
|
"menu_options": {
|
||||||
|
"intent_migrate": "Migrate to a new radio",
|
||||||
|
"intent_reconfigure": "Re-configure the current radio"
|
||||||
|
},
|
||||||
|
"title": "Migrate or re-configure"
|
||||||
|
},
|
||||||
"upload_manual_backup": {
|
"upload_manual_backup": {
|
||||||
"data": {
|
"data": {
|
||||||
"uploaded_backup_file": "Upload a file"
|
"uploaded_backup_file": "Upload a file"
|
||||||
|
@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2022
|
MAJOR_VERSION: Final = 2022
|
||||||
MINOR_VERSION: Final = 10
|
MINOR_VERSION: Final = 10
|
||||||
PATCH_VERSION: Final = "0"
|
PATCH_VERSION: Final = "1"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||||
|
@ -21,7 +21,7 @@ dbus-fast==1.24.0
|
|||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
hass-nabucasa==0.56.0
|
hass-nabucasa==0.56.0
|
||||||
home-assistant-bluetooth==1.3.0
|
home-assistant-bluetooth==1.3.0
|
||||||
home-assistant-frontend==20221005.0
|
home-assistant-frontend==20221006.0
|
||||||
httpx==0.23.0
|
httpx==0.23.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2022.10.0"
|
version = "2022.10.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -475,7 +475,7 @@ bthome-ble==1.2.2
|
|||||||
bthomehub5-devicelist==0.1.1
|
bthomehub5-devicelist==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.bt_smarthub
|
# homeassistant.components.bt_smarthub
|
||||||
btsmarthub_devicelist==0.2.2
|
btsmarthub_devicelist==0.2.3
|
||||||
|
|
||||||
# homeassistant.components.buienradar
|
# homeassistant.components.buienradar
|
||||||
buienradar==1.0.5
|
buienradar==1.0.5
|
||||||
@ -865,7 +865,7 @@ hole==0.7.0
|
|||||||
holidays==0.16
|
holidays==0.16
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20221005.0
|
home-assistant-frontend==20221006.0
|
||||||
|
|
||||||
# homeassistant.components.home_connect
|
# homeassistant.components.home_connect
|
||||||
homeconnect==0.7.2
|
homeconnect==0.7.2
|
||||||
|
@ -645,7 +645,7 @@ hole==0.7.0
|
|||||||
holidays==0.16
|
holidays==0.16
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20221005.0
|
home-assistant-frontend==20221006.0
|
||||||
|
|
||||||
# homeassistant.components.home_connect
|
# homeassistant.components.home_connect
|
||||||
homeconnect==0.7.2
|
homeconnect==0.7.2
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
from unittest.mock import ANY, patch
|
from unittest.mock import ANY, patch
|
||||||
|
|
||||||
from bleak.backends.scanner import BLEDevice
|
from bleak.backends.scanner import AdvertisementData, BLEDevice
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS
|
from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS
|
||||||
|
|
||||||
|
from . import inject_advertisement
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||||
|
|
||||||
@ -158,6 +160,10 @@ async def test_diagnostics_macos(
|
|||||||
# because we cannot import the scanner class directly without it throwing an
|
# because we cannot import the scanner class directly without it throwing an
|
||||||
# error if the test is not running on linux since we won't have the correct
|
# error if the test is not running on linux since we won't have the correct
|
||||||
# deps installed when testing on MacOS.
|
# deps installed when testing on MacOS.
|
||||||
|
switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
|
||||||
|
switchbot_adv = AdvertisementData(
|
||||||
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
|
||||||
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices",
|
"homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices",
|
||||||
@ -180,6 +186,8 @@ async def test_diagnostics_macos(
|
|||||||
assert await hass.config_entries.async_setup(entry1.entry_id)
|
assert await hass.config_entries.async_setup(entry1.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
inject_advertisement(hass, switchbot_device, switchbot_adv)
|
||||||
|
|
||||||
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
|
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
|
||||||
assert diag == {
|
assert diag == {
|
||||||
"adapters": {
|
"adapters": {
|
||||||
@ -197,8 +205,34 @@ async def test_diagnostics_macos(
|
|||||||
"sw_version": ANY,
|
"sw_version": ANY,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connectable_history": [],
|
"connectable_history": [
|
||||||
"history": [],
|
{
|
||||||
|
"address": "44:44:33:11:23:45",
|
||||||
|
"advertisement": ANY,
|
||||||
|
"connectable": True,
|
||||||
|
"manufacturer_data": ANY,
|
||||||
|
"name": "wohand",
|
||||||
|
"rssi": 0,
|
||||||
|
"service_data": {},
|
||||||
|
"service_uuids": [],
|
||||||
|
"source": "local",
|
||||||
|
"time": ANY,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"address": "44:44:33:11:23:45",
|
||||||
|
"advertisement": ANY,
|
||||||
|
"connectable": True,
|
||||||
|
"manufacturer_data": ANY,
|
||||||
|
"name": "wohand",
|
||||||
|
"rssi": 0,
|
||||||
|
"service_data": {},
|
||||||
|
"service_uuids": [],
|
||||||
|
"source": "local",
|
||||||
|
"time": ANY,
|
||||||
|
}
|
||||||
|
],
|
||||||
"scanners": [
|
"scanners": [
|
||||||
{
|
{
|
||||||
"adapter": "Core Bluetooth",
|
"adapter": "Core Bluetooth",
|
||||||
|
@ -238,8 +238,8 @@ def test_compile_hourly_statistics_purged_state_changes(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES])
|
@pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES])
|
||||||
def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes):
|
def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes):
|
||||||
"""Test compiling hourly statistics for unsupported sensor."""
|
"""Test compiling hourly statistics for sensor with unit not matching device class."""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -286,6 +286,24 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
|||||||
"statistics_unit_of_measurement": "°C",
|
"statistics_unit_of_measurement": "°C",
|
||||||
"unit_class": "temperature",
|
"unit_class": "temperature",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"has_mean": True,
|
||||||
|
"has_sum": False,
|
||||||
|
"name": None,
|
||||||
|
"source": "recorder",
|
||||||
|
"statistic_id": "sensor.test2",
|
||||||
|
"statistics_unit_of_measurement": "invalid",
|
||||||
|
"unit_class": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"has_mean": True,
|
||||||
|
"has_sum": False,
|
||||||
|
"name": None,
|
||||||
|
"source": "recorder",
|
||||||
|
"statistic_id": "sensor.test3",
|
||||||
|
"statistics_unit_of_measurement": None,
|
||||||
|
"unit_class": None,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test6",
|
"statistic_id": "sensor.test6",
|
||||||
"has_mean": True,
|
"has_mean": True,
|
||||||
@ -320,6 +338,32 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
|||||||
"sum": None,
|
"sum": None,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"sensor.test2": [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test2",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero),
|
||||||
|
"end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)),
|
||||||
|
"mean": 13.05084745762712,
|
||||||
|
"min": -10.0,
|
||||||
|
"max": 30.0,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": None,
|
||||||
|
"sum": None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sensor.test3": [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test3",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero),
|
||||||
|
"end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)),
|
||||||
|
"mean": 13.05084745762712,
|
||||||
|
"min": -10.0,
|
||||||
|
"max": 30.0,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": None,
|
||||||
|
"sum": None,
|
||||||
|
}
|
||||||
|
],
|
||||||
"sensor.test6": [
|
"sensor.test6": [
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test6",
|
"statistic_id": "sensor.test6",
|
||||||
@ -835,32 +879,44 @@ def test_compile_hourly_sum_statistics_nan_inf_state(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"entity_id,warning_1,warning_2",
|
"entity_id, device_class, state_unit, display_unit, statistics_unit, unit_class, offset, warning_1, warning_2",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"sensor.test1",
|
"sensor.test1",
|
||||||
|
"energy",
|
||||||
|
"kWh",
|
||||||
|
"kWh",
|
||||||
|
"kWh",
|
||||||
|
"energy",
|
||||||
|
0,
|
||||||
"",
|
"",
|
||||||
"bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue",
|
"bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"sensor.power_consumption",
|
"sensor.power_consumption",
|
||||||
|
"power",
|
||||||
|
"W",
|
||||||
|
"W",
|
||||||
|
"W",
|
||||||
|
"power",
|
||||||
|
15,
|
||||||
"from integration demo ",
|
"from integration demo ",
|
||||||
"bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+demo%22",
|
"bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+demo%22",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"sensor.custom_sensor",
|
"sensor.custom_sensor",
|
||||||
|
"energy",
|
||||||
|
"kWh",
|
||||||
|
"kWh",
|
||||||
|
"kWh",
|
||||||
|
"energy",
|
||||||
|
0,
|
||||||
"from integration test ",
|
"from integration test ",
|
||||||
"report it to the custom integration author",
|
"report it to the custom integration author",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("state_class", ["total_increasing"])
|
@pytest.mark.parametrize("state_class", ["total_increasing"])
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, factor",
|
|
||||||
[
|
|
||||||
("energy", "kWh", "kWh", "kWh", "energy", 1),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_compile_hourly_sum_statistics_negative_state(
|
def test_compile_hourly_sum_statistics_negative_state(
|
||||||
hass_recorder,
|
hass_recorder,
|
||||||
caplog,
|
caplog,
|
||||||
@ -873,7 +929,7 @@ def test_compile_hourly_sum_statistics_negative_state(
|
|||||||
display_unit,
|
display_unit,
|
||||||
statistics_unit,
|
statistics_unit,
|
||||||
unit_class,
|
unit_class,
|
||||||
factor,
|
offset,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics with negative states."""
|
"""Test compiling hourly statistics with negative states."""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
@ -938,8 +994,8 @@ def test_compile_hourly_sum_statistics_negative_state(
|
|||||||
"mean": None,
|
"mean": None,
|
||||||
"min": None,
|
"min": None,
|
||||||
"last_reset": None,
|
"last_reset": None,
|
||||||
"state": approx(factor * seq[7]),
|
"state": approx(seq[7]),
|
||||||
"sum": approx(factor * 15), # (15 - 10) + (10 - 0)
|
"sum": approx(offset + 15), # (20 - 15) + (10 - 0)
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
@ -1844,12 +1900,13 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
"device_class, state_unit, state_unit2, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, None, None, None, None, 13.050847, -10, 30),
|
(None, None, "cats", None, 13.050847, -10, 30),
|
||||||
(None, "%", "%", "%", None, 13.050847, -10, 30),
|
(None, "%", "cats", None, 13.050847, -10, 30),
|
||||||
("battery", "%", "%", "%", None, 13.050847, -10, 30),
|
("battery", "%", "cats", None, 13.050847, -10, 30),
|
||||||
("battery", None, None, None, None, 13.050847, -10, 30),
|
("battery", None, "cats", None, 13.050847, -10, 30),
|
||||||
|
(None, "kW", "Wh", "power", 13.050847, -10, 30),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compile_hourly_statistics_changing_units_1(
|
def test_compile_hourly_statistics_changing_units_1(
|
||||||
@ -1857,8 +1914,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
caplog,
|
caplog,
|
||||||
device_class,
|
device_class,
|
||||||
state_unit,
|
state_unit,
|
||||||
display_unit,
|
state_unit2,
|
||||||
statistics_unit,
|
|
||||||
unit_class,
|
unit_class,
|
||||||
mean,
|
mean,
|
||||||
min,
|
min,
|
||||||
@ -1875,7 +1931,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
"unit_of_measurement": state_unit,
|
"unit_of_measurement": state_unit,
|
||||||
}
|
}
|
||||||
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
||||||
attributes["unit_of_measurement"] = "cats"
|
attributes["unit_of_measurement"] = state_unit2
|
||||||
four, _states = record_states(
|
four, _states = record_states(
|
||||||
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
@ -1889,7 +1945,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
assert "does not match the unit of already compiled" not in caplog.text
|
assert "can not be converted to the unit of previously" not in caplog.text
|
||||||
statistic_ids = list_statistic_ids(hass)
|
statistic_ids = list_statistic_ids(hass)
|
||||||
assert statistic_ids == [
|
assert statistic_ids == [
|
||||||
{
|
{
|
||||||
@ -1898,7 +1954,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
"has_sum": False,
|
"has_sum": False,
|
||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": statistics_unit,
|
"statistics_unit_of_measurement": state_unit,
|
||||||
"unit_class": unit_class,
|
"unit_class": unit_class,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -1922,8 +1978,8 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
do_adhoc_statistics(hass, start=zero + timedelta(minutes=10))
|
do_adhoc_statistics(hass, start=zero + timedelta(minutes=10))
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
assert (
|
assert (
|
||||||
"The unit of sensor.test1 (cats) does not match the unit of already compiled "
|
f"The unit of sensor.test1 ({state_unit2}) can not be converted to the unit of "
|
||||||
f"statistics ({display_unit})" in caplog.text
|
f"previously compiled statistics ({state_unit})" in caplog.text
|
||||||
)
|
)
|
||||||
statistic_ids = list_statistic_ids(hass)
|
statistic_ids = list_statistic_ids(hass)
|
||||||
assert statistic_ids == [
|
assert statistic_ids == [
|
||||||
@ -1933,7 +1989,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
"has_sum": False,
|
"has_sum": False,
|
||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": statistics_unit,
|
"statistics_unit_of_measurement": state_unit,
|
||||||
"unit_class": unit_class,
|
"unit_class": unit_class,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -3039,18 +3095,30 @@ def record_states(hass, zero, entity_id, attributes, seq=None):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"units, attributes, unit",
|
"units, attributes, unit, unit2, supported_unit",
|
||||||
[
|
[
|
||||||
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
|
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
|
||||||
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
|
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
|
||||||
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"),
|
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"),
|
||||||
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"),
|
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"),
|
||||||
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"),
|
(
|
||||||
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"),
|
IMPERIAL_SYSTEM,
|
||||||
|
PRESSURE_SENSOR_ATTRIBUTES,
|
||||||
|
"psi",
|
||||||
|
"bar",
|
||||||
|
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
METRIC_SYSTEM,
|
||||||
|
PRESSURE_SENSOR_ATTRIBUTES,
|
||||||
|
"Pa",
|
||||||
|
"bar",
|
||||||
|
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_validate_statistics_supported_device_class(
|
async def test_validate_statistics_unit_change_device_class(
|
||||||
hass, hass_ws_client, recorder_mock, units, attributes, unit
|
hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit
|
||||||
):
|
):
|
||||||
"""Test validate_statistics."""
|
"""Test validate_statistics."""
|
||||||
id = 1
|
id = 1
|
||||||
@ -3078,39 +3146,40 @@ async def test_validate_statistics_supported_device_class(
|
|||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# No statistics, valid state - empty response
|
# No statistics, unit in state matching device class - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}}
|
"sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}}
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# No statistics, invalid state - expect error
|
# No statistics, unit in state not matching device class - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
"sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
expected = {
|
await assert_validation_result(client, {})
|
||||||
"sensor.test": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"device_class": attributes["device_class"],
|
|
||||||
"state_unit": "dogs",
|
|
||||||
"statistic_id": "sensor.test",
|
|
||||||
},
|
|
||||||
"type": "unsupported_unit_state",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
await assert_validation_result(client, expected)
|
|
||||||
|
|
||||||
# Statistics has run, invalid state - expect error
|
# Statistics has run, incompatible unit - expect error
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
"sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
|
expected = {
|
||||||
|
"sensor.test": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"metadata_unit": unit,
|
||||||
|
"state_unit": "dogs",
|
||||||
|
"statistic_id": "sensor.test",
|
||||||
|
"supported_unit": supported_unit,
|
||||||
|
},
|
||||||
|
"type": "units_changed",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
await assert_validation_result(client, expected)
|
await assert_validation_result(client, expected)
|
||||||
|
|
||||||
# Valid state - empty response
|
# Valid state - empty response
|
||||||
@ -3125,6 +3194,18 @@ async def test_validate_statistics_supported_device_class(
|
|||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Valid state in compatible unit - empty response
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}}
|
||||||
|
)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Valid state, statistic runs again - empty response
|
||||||
|
do_adhoc_statistics(hass, start=now)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# Remove the state - empty response
|
# Remove the state - empty response
|
||||||
hass.states.async_remove("sensor.test")
|
hass.states.async_remove("sensor.test")
|
||||||
expected = {
|
expected = {
|
||||||
@ -3144,7 +3225,7 @@ async def test_validate_statistics_supported_device_class(
|
|||||||
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"),
|
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_validate_statistics_supported_device_class_2(
|
async def test_validate_statistics_unit_change_device_class_2(
|
||||||
hass, hass_ws_client, recorder_mock, units, attributes, valid_units
|
hass, hass_ws_client, recorder_mock, units, attributes, valid_units
|
||||||
):
|
):
|
||||||
"""Test validate_statistics."""
|
"""Test validate_statistics."""
|
||||||
@ -3173,56 +3254,144 @@ async def test_validate_statistics_supported_device_class_2(
|
|||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# No statistics, valid state - empty response
|
# No statistics, no device class - empty response
|
||||||
initial_attributes = {"state_class": "measurement"}
|
initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"}
|
||||||
hass.states.async_set("sensor.test", 10, attributes=initial_attributes)
|
hass.states.async_set("sensor.test", 10, attributes=initial_attributes)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# Statistics has run, device class set - expect error
|
# Statistics has run, device class set not matching unit - empty response
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
hass.states.async_set("sensor.test", 12, attributes=attributes)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
expected = {
|
|
||||||
"sensor.test": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"device_class": attributes["device_class"],
|
|
||||||
"metadata_unit": None,
|
|
||||||
"statistic_id": "sensor.test",
|
|
||||||
"supported_unit": valid_units,
|
|
||||||
},
|
|
||||||
"type": "unsupported_unit_metadata",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
await assert_validation_result(client, expected)
|
|
||||||
|
|
||||||
# Invalid state too, expect double errors
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
"sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"units, attributes, unit, unit2, supported_unit",
|
||||||
|
[
|
||||||
|
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
|
||||||
|
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
|
||||||
|
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"),
|
||||||
|
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"),
|
||||||
|
(
|
||||||
|
IMPERIAL_SYSTEM,
|
||||||
|
PRESSURE_SENSOR_ATTRIBUTES,
|
||||||
|
"psi",
|
||||||
|
"bar",
|
||||||
|
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
METRIC_SYSTEM,
|
||||||
|
PRESSURE_SENSOR_ATTRIBUTES,
|
||||||
|
"Pa",
|
||||||
|
"bar",
|
||||||
|
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_validate_statistics_unit_change_no_device_class(
|
||||||
|
hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit
|
||||||
|
):
|
||||||
|
"""Test validate_statistics."""
|
||||||
|
id = 1
|
||||||
|
attributes = dict(attributes)
|
||||||
|
attributes.pop("device_class")
|
||||||
|
|
||||||
|
def next_id():
|
||||||
|
nonlocal id
|
||||||
|
id += 1
|
||||||
|
return id
|
||||||
|
|
||||||
|
async def assert_validation_result(client, expected_result):
|
||||||
|
await client.send_json(
|
||||||
|
{"id": next_id(), "type": "recorder/validate_statistics"}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == expected_result
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
hass.config.units = units
|
||||||
|
await async_setup_component(hass, "sensor", {})
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
client = await hass_ws_client()
|
||||||
|
|
||||||
|
# No statistics, no state - empty response
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# No statistics, unit in state matching device class - empty response
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}}
|
||||||
|
)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# No statistics, unit in state not matching device class - empty response
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
||||||
|
)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Statistics has run, incompatible unit - expect error
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
do_adhoc_statistics(hass, start=now)
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
expected = {
|
expected = {
|
||||||
"sensor.test": [
|
"sensor.test": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"device_class": attributes["device_class"],
|
"metadata_unit": unit,
|
||||||
"metadata_unit": None,
|
|
||||||
"statistic_id": "sensor.test",
|
|
||||||
"supported_unit": valid_units,
|
|
||||||
},
|
|
||||||
"type": "unsupported_unit_metadata",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"device_class": attributes["device_class"],
|
|
||||||
"state_unit": "dogs",
|
"state_unit": "dogs",
|
||||||
"statistic_id": "sensor.test",
|
"statistic_id": "sensor.test",
|
||||||
|
"supported_unit": supported_unit,
|
||||||
},
|
},
|
||||||
"type": "unsupported_unit_state",
|
"type": "units_changed",
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
await assert_validation_result(client, expected)
|
||||||
|
|
||||||
|
# Valid state - empty response
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}}
|
||||||
|
)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Valid state, statistic runs again - empty response
|
||||||
|
do_adhoc_statistics(hass, start=now)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Valid state in compatible unit - empty response
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}}
|
||||||
|
)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Valid state, statistic runs again - empty response
|
||||||
|
do_adhoc_statistics(hass, start=now)
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
|
# Remove the state - empty response
|
||||||
|
hass.states.async_remove("sensor.test")
|
||||||
|
expected = {
|
||||||
|
"sensor.test": [
|
||||||
|
{
|
||||||
|
"data": {"statistic_id": "sensor.test"},
|
||||||
|
"type": "no_state",
|
||||||
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
await assert_validation_result(client, expected)
|
await assert_validation_result(client, expected)
|
||||||
@ -3473,7 +3642,7 @@ async def test_validate_statistics_sensor_removed(
|
|||||||
"attributes",
|
"attributes",
|
||||||
[BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES],
|
[BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES],
|
||||||
)
|
)
|
||||||
async def test_validate_statistics_unsupported_device_class(
|
async def test_validate_statistics_unit_change_no_conversion(
|
||||||
hass, recorder_mock, hass_ws_client, attributes
|
hass, recorder_mock, hass_ws_client, attributes
|
||||||
):
|
):
|
||||||
"""Test validate_statistics."""
|
"""Test validate_statistics."""
|
||||||
@ -3553,6 +3722,7 @@ async def test_validate_statistics_unsupported_device_class(
|
|||||||
"metadata_unit": "dogs",
|
"metadata_unit": "dogs",
|
||||||
"state_unit": attributes.get("unit_of_measurement"),
|
"state_unit": attributes.get("unit_of_measurement"),
|
||||||
"statistic_id": "sensor.test",
|
"statistic_id": "sensor.test",
|
||||||
|
"supported_unit": "dogs",
|
||||||
},
|
},
|
||||||
"type": "units_changed",
|
"type": "units_changed",
|
||||||
}
|
}
|
||||||
@ -3573,124 +3743,7 @@ async def test_validate_statistics_unsupported_device_class(
|
|||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(client, {})
|
||||||
|
|
||||||
# Remove the state - empty response
|
# Remove the state - expect error
|
||||||
hass.states.async_remove("sensor.test")
|
|
||||||
expected = {
|
|
||||||
"sensor.test": [
|
|
||||||
{
|
|
||||||
"data": {"statistic_id": "sensor.test"},
|
|
||||||
"type": "no_state",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
await assert_validation_result(client, expected)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"attributes",
|
|
||||||
[KW_SENSOR_ATTRIBUTES],
|
|
||||||
)
|
|
||||||
async def test_validate_statistics_unsupported_device_class_2(
|
|
||||||
hass, recorder_mock, hass_ws_client, attributes
|
|
||||||
):
|
|
||||||
"""Test validate_statistics."""
|
|
||||||
id = 1
|
|
||||||
|
|
||||||
def next_id():
|
|
||||||
nonlocal id
|
|
||||||
id += 1
|
|
||||||
return id
|
|
||||||
|
|
||||||
async def assert_validation_result(client, expected_result):
|
|
||||||
await client.send_json(
|
|
||||||
{"id": next_id(), "type": "recorder/validate_statistics"}
|
|
||||||
)
|
|
||||||
response = await client.receive_json()
|
|
||||||
assert response["success"]
|
|
||||||
assert response["result"] == expected_result
|
|
||||||
|
|
||||||
async def assert_statistic_ids(expected_result):
|
|
||||||
with session_scope(hass=hass) as session:
|
|
||||||
db_states = list(session.query(StatisticsMeta))
|
|
||||||
assert len(db_states) == len(expected_result)
|
|
||||||
for i in range(len(db_states)):
|
|
||||||
assert db_states[i].statistic_id == expected_result[i]["statistic_id"]
|
|
||||||
assert (
|
|
||||||
db_states[i].unit_of_measurement
|
|
||||||
== expected_result[i]["unit_of_measurement"]
|
|
||||||
)
|
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", {})
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
client = await hass_ws_client()
|
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# No statistics, original unit - empty response
|
|
||||||
hass.states.async_set("sensor.test", 10, attributes=attributes)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# No statistics, changed unit - empty response
|
|
||||||
hass.states.async_set(
|
|
||||||
"sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "W"}}
|
|
||||||
)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# Run statistics, no statistics will be generated because of conflicting units
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
do_adhoc_statistics(hass, start=now)
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
await assert_statistic_ids([])
|
|
||||||
|
|
||||||
# No statistics, changed unit - empty response
|
|
||||||
hass.states.async_set(
|
|
||||||
"sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "W"}}
|
|
||||||
)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# Run statistics one hour later, only the "W" state will be considered
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
await assert_statistic_ids(
|
|
||||||
[{"statistic_id": "sensor.test", "unit_of_measurement": "W"}]
|
|
||||||
)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# Change back to original unit - expect error
|
|
||||||
hass.states.async_set("sensor.test", 13, attributes=attributes)
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
expected = {
|
|
||||||
"sensor.test": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"metadata_unit": "W",
|
|
||||||
"state_unit": "kW",
|
|
||||||
"statistic_id": "sensor.test",
|
|
||||||
},
|
|
||||||
"type": "units_changed_can_convert",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
await assert_validation_result(client, expected)
|
|
||||||
|
|
||||||
# Changed unit - empty response
|
|
||||||
hass.states.async_set(
|
|
||||||
"sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "W"}}
|
|
||||||
)
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# Valid state, statistic runs again - empty response
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
do_adhoc_statistics(hass, start=now)
|
|
||||||
await async_recorder_block_till_done(hass)
|
|
||||||
await assert_validation_result(client, {})
|
|
||||||
|
|
||||||
# Remove the state - empty response
|
|
||||||
hass.states.async_remove("sensor.test")
|
hass.states.async_remove("sensor.test")
|
||||||
expected = {
|
expected = {
|
||||||
"sensor.test": [
|
"sensor.test": [
|
||||||
|
@ -46,6 +46,13 @@ def disable_platform_only():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reduce_reconnect_timeout():
|
||||||
|
"""Reduces reconnect timeout to speed up tests."""
|
||||||
|
with patch("homeassistant.components.zha.config_flow.CONNECT_DELAY_S", 0.01):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_app():
|
def mock_app():
|
||||||
"""Mock zigpy app interface."""
|
"""Mock zigpy app interface."""
|
||||||
@ -230,10 +237,10 @@ async def test_efr32_via_zeroconf(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result3["title"] == "socket://192.168.1.200:6638"
|
assert result3["title"] == "socket://192.168.1.200:1234"
|
||||||
assert result3["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_DEVICE: {
|
CONF_DEVICE: {
|
||||||
CONF_DEVICE_PATH: "socket://192.168.1.200:6638",
|
CONF_DEVICE_PATH: "socket://192.168.1.200:1234",
|
||||||
CONF_BAUDRATE: 115200,
|
CONF_BAUDRATE: 115200,
|
||||||
CONF_FLOWCONTROL: "software",
|
CONF_FLOWCONTROL: "software",
|
||||||
},
|
},
|
||||||
@ -1476,21 +1483,28 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has
|
|||||||
# Unload it ourselves
|
# Unload it ourselves
|
||||||
entry.state = config_entries.ConfigEntryState.NOT_LOADED
|
entry.state = config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
# Reconfigure ZHA
|
||||||
|
assert result1["step_id"] == "prompt_migrate_or_reconfigure"
|
||||||
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE},
|
||||||
|
)
|
||||||
|
|
||||||
# Current path is the default
|
# Current path is the default
|
||||||
assert result1["step_id"] == "choose_serial_port"
|
assert result2["step_id"] == "choose_serial_port"
|
||||||
assert "/dev/ttyUSB0" in result1["data_schema"]({})[CONF_DEVICE_PATH]
|
assert "/dev/ttyUSB0" in result2["data_schema"]({})[CONF_DEVICE_PATH]
|
||||||
|
|
||||||
# Autoprobing fails, we have to manually choose the radio type
|
# Autoprobing fails, we have to manually choose the radio type
|
||||||
result2 = await hass.config_entries.options.async_configure(
|
result3 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Current radio type is the default
|
# Current radio type is the default
|
||||||
assert result2["step_id"] == "manual_pick_radio_type"
|
assert result3["step_id"] == "manual_pick_radio_type"
|
||||||
assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description
|
assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description
|
||||||
|
|
||||||
# Continue on to port settings
|
# Continue on to port settings
|
||||||
result3 = await hass.config_entries.options.async_configure(
|
result4 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"],
|
flow["flow_id"],
|
||||||
user_input={
|
user_input={
|
||||||
CONF_RADIO_TYPE: RadioType.znp.description,
|
CONF_RADIO_TYPE: RadioType.znp.description,
|
||||||
@ -1498,12 +1512,12 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has
|
|||||||
)
|
)
|
||||||
|
|
||||||
# The defaults match our current settings
|
# The defaults match our current settings
|
||||||
assert result3["step_id"] == "manual_port_config"
|
assert result4["step_id"] == "manual_port_config"
|
||||||
assert result3["data_schema"]({}) == entry.data[CONF_DEVICE]
|
assert result4["data_schema"]({}) == entry.data[CONF_DEVICE]
|
||||||
|
|
||||||
with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)):
|
with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)):
|
||||||
# Change the serial port path
|
# Change the serial port path
|
||||||
result4 = await hass.config_entries.options.async_configure(
|
result5 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"],
|
flow["flow_id"],
|
||||||
user_input={
|
user_input={
|
||||||
# Change everything
|
# Change everything
|
||||||
@ -1514,18 +1528,18 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has
|
|||||||
)
|
)
|
||||||
|
|
||||||
# The radio has been detected, we can move on to creating the config entry
|
# The radio has been detected, we can move on to creating the config entry
|
||||||
assert result4["step_id"] == "choose_formation_strategy"
|
assert result5["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
async_setup_entry.assert_not_called()
|
async_setup_entry.assert_not_called()
|
||||||
|
|
||||||
result5 = await hass.config_entries.options.async_configure(
|
result6 = await hass.config_entries.options.async_configure(
|
||||||
result1["flow_id"],
|
result1["flow_id"],
|
||||||
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result5["type"] == FlowResultType.CREATE_ENTRY
|
assert result6["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result5["data"] == {}
|
assert result6["data"] == {}
|
||||||
|
|
||||||
# The updated entry contains correct settings
|
# The updated entry contains correct settings
|
||||||
assert entry.data == {
|
assert entry.data == {
|
||||||
@ -1581,33 +1595,39 @@ async def test_options_flow_defaults_socket(hass):
|
|||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Radio path must be manually entered
|
assert result1["step_id"] == "prompt_migrate_or_reconfigure"
|
||||||
assert result1["step_id"] == "choose_serial_port"
|
|
||||||
assert result1["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.options.async_configure(
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"],
|
||||||
|
user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Current radio type is the default
|
# Radio path must be manually entered
|
||||||
assert result2["step_id"] == "manual_pick_radio_type"
|
assert result2["step_id"] == "choose_serial_port"
|
||||||
assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description
|
assert result2["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH
|
||||||
|
|
||||||
# Continue on to port settings
|
|
||||||
result3 = await hass.config_entries.options.async_configure(
|
result3 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Current radio type is the default
|
||||||
|
assert result3["step_id"] == "manual_pick_radio_type"
|
||||||
|
assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description
|
||||||
|
|
||||||
|
# Continue on to port settings
|
||||||
|
result4 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
# The defaults match our current settings
|
# The defaults match our current settings
|
||||||
assert result3["step_id"] == "manual_port_config"
|
assert result4["step_id"] == "manual_port_config"
|
||||||
assert result3["data_schema"]({}) == entry.data[CONF_DEVICE]
|
assert result4["data_schema"]({}) == entry.data[CONF_DEVICE]
|
||||||
|
|
||||||
with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)):
|
with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)):
|
||||||
result4 = await hass.config_entries.options.async_configure(
|
result5 = await hass.config_entries.options.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result4["step_id"] == "choose_formation_strategy"
|
assert result5["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.zha.async_setup_entry", return_value=True)
|
@patch("homeassistant.components.zha.async_setup_entry", return_value=True)
|
||||||
@ -1643,14 +1663,82 @@ async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry,
|
|||||||
|
|
||||||
entry.state = config_entries.ConfigEntryState.NOT_LOADED
|
entry.state = config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
assert result1["step_id"] == "prompt_migrate_or_reconfigure"
|
||||||
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE},
|
||||||
|
)
|
||||||
|
|
||||||
# Radio path must be manually entered
|
# Radio path must be manually entered
|
||||||
assert result1["step_id"] == "choose_serial_port"
|
assert result2["step_id"] == "choose_serial_port"
|
||||||
|
|
||||||
async_setup_entry.reset_mock()
|
async_setup_entry.reset_mock()
|
||||||
|
|
||||||
# Abort the flow
|
# Abort the flow
|
||||||
hass.config_entries.options.async_abort(result1["flow_id"])
|
hass.config_entries.options.async_abort(result2["flow_id"])
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# ZHA was set up once more
|
# ZHA was set up once more
|
||||||
async_setup_entry.assert_called_once_with(hass, entry)
|
async_setup_entry.assert_called_once_with(hass, entry)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
|
||||||
|
async def test_options_flow_migration_reset_old_adapter(hass, mock_app):
|
||||||
|
"""Test options flow for migrating from an old radio."""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
version=config_flow.ZhaConfigFlowHandler.VERSION,
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_DEVICE: {
|
||||||
|
CONF_DEVICE_PATH: "/dev/serial/by-id/old_radio",
|
||||||
|
CONF_BAUDRATE: 12345,
|
||||||
|
CONF_FLOWCONTROL: None,
|
||||||
|
},
|
||||||
|
CONF_RADIO_TYPE: "znp",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
flow = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
# ZHA gets unloaded
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config_entries.ConfigEntries.async_unload", return_value=True
|
||||||
|
):
|
||||||
|
result1 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.state = config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
assert result1["step_id"] == "prompt_migrate_or_reconfigure"
|
||||||
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
user_input={"next_step_id": config_flow.OPTIONS_INTENT_MIGRATE},
|
||||||
|
)
|
||||||
|
|
||||||
|
# User must explicitly approve radio reset
|
||||||
|
assert result2["step_id"] == "intent_migrate"
|
||||||
|
|
||||||
|
mock_app.reset_network_info = AsyncMock()
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
user_input={},
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_app.reset_network_info.assert_awaited_once()
|
||||||
|
|
||||||
|
# Now we can unplug the old radio
|
||||||
|
assert result3["step_id"] == "instruct_unplug"
|
||||||
|
|
||||||
|
# And move on to choosing the new radio
|
||||||
|
result4 = await hass.config_entries.options.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
user_input={},
|
||||||
|
)
|
||||||
|
assert result4["step_id"] == "choose_serial_port"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user