mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 20:34:52 +02:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddb0a6092f | |||
| 292c05ab9f | |||
| a900c02c10 | |||
| 0cfbff3ff9 | |||
| ee892beceb | |||
| fe713b943f | |||
| 2079956350 | |||
| c424f99aab | |||
| 5cc54618c5 | |||
| 8f344252c4 | |||
| cbe4b2dc1d | |||
| a17d2d7c71 | |||
| e3815c6c2e | |||
| 5cba7932f3 | |||
| 413430bdba | |||
| 81462d8655 | |||
| 8ee4b49aa9 | |||
| 19d7cb4439 | |||
| 21ebf4f3e6 | |||
| 980fcef36f | |||
| e7fd24eade | |||
| 9ecb75dc70 | |||
| 7f3adce675 | |||
| f0649855f9 | |||
| 823c3735ce | |||
| 68131a5c00 | |||
| be0f767c34 | |||
| 450652a501 | |||
| 8523f569c0 | |||
| 5f289434d3 | |||
| 3df6dfecab | |||
| d6eda65302 | |||
| 7a5bc2784a | |||
| 00878467cc | |||
| 899d8164b0 | |||
| 823fd60991 | |||
| a6bb0eadca | |||
| eb70354ee7 | |||
| bd53185bed | |||
| df9a899bbd | |||
| 37cf295e20 | |||
| 04816fe26d | |||
| eb48e75fc5 | |||
| 9d5431fba1 | |||
| a4f2c5583d | |||
| a37c3af2b4 | |||
| 33047d7260 | |||
| e3405d226a | |||
| 3008ff03b2 | |||
| 8592d94a3c | |||
| b36e86d95c | |||
| f61a1ecae7 | |||
| 80c074ca82 | |||
| ff91ff4cd2 | |||
| 93c2a7dd70 | |||
| da3ee9ed4b | |||
| 2ef607651d | |||
| 88ca83a30b |
@@ -580,7 +580,7 @@ jobs:
|
||||
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -U "pip<20.3" setuptools wheel
|
||||
pip install -U "pip<20.3" "setuptools<58" wheel
|
||||
pip install -r requirements_all.txt
|
||||
pip install -r requirements_test.txt
|
||||
pip install -e .
|
||||
|
||||
@@ -342,7 +342,11 @@ def async_enable_logging(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
err_handler.doRollover()
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "amcrest",
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/integrations/amcrest",
|
||||
"requirements": ["amcrest==1.8.0"],
|
||||
"requirements": ["amcrest==1.8.1"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": ["@flacjacket"],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Iterable, Mapping
|
||||
from functools import wraps
|
||||
import logging
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
@@ -27,7 +28,6 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig
|
||||
|
||||
DOMAIN = "device_automation"
|
||||
|
||||
|
||||
DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "device",
|
||||
@@ -174,6 +174,13 @@ async def _async_get_device_automations(
|
||||
device_results, InvalidDeviceAutomationConfig
|
||||
):
|
||||
continue
|
||||
if isinstance(device_results, Exception):
|
||||
logging.getLogger(__name__).error(
|
||||
"Unexpected error fetching device %ss",
|
||||
automation_type,
|
||||
exc_info=device_results,
|
||||
)
|
||||
continue
|
||||
for automation in device_results:
|
||||
combined_results[automation["device_id"]].append(automation)
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ from homeassistant.components.device_automation import (
|
||||
)
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
|
||||
from .exceptions import InvalidDeviceAutomationConfig
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -17,10 +19,13 @@ async def async_validate_trigger_config(hass, config):
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], "trigger"
|
||||
)
|
||||
if hasattr(platform, "async_validate_trigger_config"):
|
||||
return await getattr(platform, "async_validate_trigger_config")(hass, config)
|
||||
if not hasattr(platform, "async_validate_trigger_config"):
|
||||
return platform.TRIGGER_SCHEMA(config)
|
||||
|
||||
return platform.TRIGGER_SCHEMA(config)
|
||||
try:
|
||||
return await getattr(platform, "async_validate_trigger_config")(hass, config)
|
||||
except InvalidDeviceAutomationConfig as err:
|
||||
raise vol.Invalid(str(err) or "Invalid trigger configuration") from err
|
||||
|
||||
|
||||
async def async_attach_trigger(hass, config, action, automation_info):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
from typing import Callable, Final
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -24,6 +24,9 @@ from homeassistant.const import (
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
|
||||
PRICE_EUR_KWH: Final = f"EUR/{ENERGY_KILO_WATT_HOUR}"
|
||||
PRICE_EUR_M3: Final = f"EUR/{VOLUME_CUBIC_METERS}"
|
||||
|
||||
|
||||
def dsmr_transform(value):
|
||||
"""Transform DSMR version value to right format."""
|
||||
@@ -301,31 +304,31 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_1",
|
||||
name="Low tariff delivered price",
|
||||
icon="mdi:currency-eur",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
native_unit_of_measurement=PRICE_EUR_KWH,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_2",
|
||||
name="High tariff delivered price",
|
||||
icon="mdi:currency-eur",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
native_unit_of_measurement=PRICE_EUR_KWH,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/energy_supplier_price_electricity_returned_1",
|
||||
name="Low tariff returned price",
|
||||
icon="mdi:currency-eur",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
native_unit_of_measurement=PRICE_EUR_KWH,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/energy_supplier_price_electricity_returned_2",
|
||||
name="High tariff returned price",
|
||||
icon="mdi:currency-eur",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
native_unit_of_measurement=PRICE_EUR_KWH,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/energy_supplier_price_gas",
|
||||
name="Gas price",
|
||||
icon="mdi:currency-eur",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
native_unit_of_measurement=PRICE_EUR_M3,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/fixed_cost",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"""Helper sensor for calculating utility costs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Final, Literal, TypeVar, cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_STATE_CLASS,
|
||||
DEVICE_CLASS_MONETARY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
SensorEntity,
|
||||
)
|
||||
@@ -18,14 +21,19 @@ from homeassistant.const import (
|
||||
ENERGY_WATT_HOUR,
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import EnergyManager, async_get_manager
|
||||
|
||||
SUPPORTED_STATE_CLASSES = [
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -206,15 +214,16 @@ class EnergyCostSensor(SensorEntity):
|
||||
f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}"
|
||||
)
|
||||
self._attr_device_class = DEVICE_CLASS_MONETARY
|
||||
self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
self._config = config
|
||||
self._last_energy_sensor_state: StateType | None = None
|
||||
self._last_energy_sensor_state: State | None = None
|
||||
self._cur_value = 0.0
|
||||
|
||||
def _reset(self, energy_state: StateType) -> None:
|
||||
def _reset(self, energy_state: State) -> None:
|
||||
"""Reset the cost sensor."""
|
||||
self._attr_native_value = 0.0
|
||||
self._cur_value = 0.0
|
||||
self._attr_last_reset = dt_util.utcnow()
|
||||
self._last_energy_sensor_state = energy_state
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -228,9 +237,8 @@ class EnergyCostSensor(SensorEntity):
|
||||
if energy_state is None:
|
||||
return
|
||||
|
||||
if (
|
||||
state_class := energy_state.attributes.get(ATTR_STATE_CLASS)
|
||||
) != STATE_CLASS_TOTAL_INCREASING:
|
||||
state_class = energy_state.attributes.get(ATTR_STATE_CLASS)
|
||||
if state_class not in SUPPORTED_STATE_CLASSES:
|
||||
if not self._wrong_state_class_reported:
|
||||
self._wrong_state_class_reported = True
|
||||
_LOGGER.warning(
|
||||
@@ -240,6 +248,13 @@ class EnergyCostSensor(SensorEntity):
|
||||
)
|
||||
return
|
||||
|
||||
# last_reset must be set if the sensor is STATE_CLASS_MEASUREMENT
|
||||
if (
|
||||
state_class == STATE_CLASS_MEASUREMENT
|
||||
and ATTR_LAST_RESET not in energy_state.attributes
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
energy = float(energy_state.state)
|
||||
except ValueError:
|
||||
@@ -273,7 +288,7 @@ class EnergyCostSensor(SensorEntity):
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state.state)
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
@@ -298,20 +313,29 @@ class EnergyCostSensor(SensorEntity):
|
||||
)
|
||||
return
|
||||
|
||||
if reset_detected(
|
||||
if state_class != STATE_CLASS_TOTAL_INCREASING and energy_state.attributes.get(
|
||||
ATTR_LAST_RESET
|
||||
) != self._last_energy_sensor_state.attributes.get(ATTR_LAST_RESET):
|
||||
# Energy meter was reset, reset cost sensor too
|
||||
energy_state_copy = copy.copy(energy_state)
|
||||
energy_state_copy.state = "0.0"
|
||||
self._reset(energy_state_copy)
|
||||
elif state_class == STATE_CLASS_TOTAL_INCREASING and reset_detected(
|
||||
self.hass,
|
||||
cast(str, self._config[self._adapter.entity_energy_key]),
|
||||
energy,
|
||||
float(self._last_energy_sensor_state),
|
||||
float(self._last_energy_sensor_state.state),
|
||||
):
|
||||
# Energy meter was reset, reset cost sensor too
|
||||
self._reset(0)
|
||||
energy_state_copy = copy.copy(energy_state)
|
||||
energy_state_copy.state = "0.0"
|
||||
self._reset(energy_state_copy)
|
||||
# Update with newly incurred cost
|
||||
old_energy_value = float(self._last_energy_sensor_state)
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
self._cur_value += (energy - old_energy_value) * energy_price
|
||||
self._attr_native_value = round(self._cur_value, 2)
|
||||
|
||||
self._last_energy_sensor_state = energy_state.state
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Validate the energy preferences provide valid data."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
@@ -10,12 +11,24 @@ from homeassistant.const import (
|
||||
ENERGY_WATT_HOUR,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
|
||||
from . import data
|
||||
from .const import DOMAIN
|
||||
|
||||
ENERGY_USAGE_UNITS = (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR)
|
||||
ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy"
|
||||
GAS_USAGE_UNITS = (
|
||||
ENERGY_WATT_HOUR,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_CUBIC_FEET,
|
||||
)
|
||||
GAS_UNIT_ERROR = "entity_unexpected_unit_gas"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ValidationIssue:
|
||||
@@ -43,8 +56,12 @@ class EnergyPreferencesValidation:
|
||||
|
||||
|
||||
@callback
|
||||
def _async_validate_energy_stat(
|
||||
hass: HomeAssistant, stat_value: str, result: list[ValidationIssue]
|
||||
def _async_validate_usage_stat(
|
||||
hass: HomeAssistant,
|
||||
stat_value: str,
|
||||
allowed_units: Sequence[str],
|
||||
unit_error: str,
|
||||
result: list[ValidationIssue],
|
||||
) -> None:
|
||||
"""Validate a statistic."""
|
||||
has_entity_source = valid_entity_id(stat_value)
|
||||
@@ -91,14 +108,16 @@ def _async_validate_energy_stat(
|
||||
|
||||
unit = state.attributes.get("unit_of_measurement")
|
||||
|
||||
if unit not in (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR):
|
||||
result.append(
|
||||
ValidationIssue("entity_unexpected_unit_energy", stat_value, unit)
|
||||
)
|
||||
if unit not in allowed_units:
|
||||
result.append(ValidationIssue(unit_error, stat_value, unit))
|
||||
|
||||
state_class = state.attributes.get("state_class")
|
||||
|
||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
||||
supported_state_classes = [
|
||||
sensor.STATE_CLASS_MEASUREMENT,
|
||||
sensor.STATE_CLASS_TOTAL_INCREASING,
|
||||
]
|
||||
if state_class not in supported_state_classes:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_unexpected_state_class_total_increasing",
|
||||
@@ -125,16 +144,13 @@ def _async_validate_price_entity(
|
||||
return
|
||||
|
||||
try:
|
||||
value: float | None = float(state.state)
|
||||
float(state.state)
|
||||
except ValueError:
|
||||
result.append(
|
||||
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
|
||||
)
|
||||
return
|
||||
|
||||
if value is not None and value < 0:
|
||||
result.append(ValidationIssue("entity_negative_state", entity_id, value))
|
||||
|
||||
unit = state.attributes.get("unit_of_measurement")
|
||||
|
||||
if unit is None or not unit.endswith(
|
||||
@@ -188,7 +204,11 @@ def _async_validate_cost_entity(
|
||||
|
||||
state_class = state.attributes.get("state_class")
|
||||
|
||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
||||
supported_state_classes = [
|
||||
sensor.STATE_CLASS_MEASUREMENT,
|
||||
sensor.STATE_CLASS_TOTAL_INCREASING,
|
||||
]
|
||||
if state_class not in supported_state_classes:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_unexpected_state_class_total_increasing", entity_id, state_class
|
||||
@@ -211,8 +231,12 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
|
||||
if source["type"] == "grid":
|
||||
for flow in source["flow_from"]:
|
||||
_async_validate_energy_stat(
|
||||
hass, flow["stat_energy_from"], source_result
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
flow["stat_energy_from"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
|
||||
if flow.get("stat_cost") is not None:
|
||||
@@ -229,7 +253,13 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
)
|
||||
|
||||
for flow in source["flow_to"]:
|
||||
_async_validate_energy_stat(hass, flow["stat_energy_to"], source_result)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
flow["stat_energy_to"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
|
||||
if flow.get("stat_compensation") is not None:
|
||||
_async_validate_cost_stat(
|
||||
@@ -247,7 +277,13 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
)
|
||||
|
||||
elif source["type"] == "gas":
|
||||
_async_validate_energy_stat(hass, source["stat_energy_from"], source_result)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
GAS_USAGE_UNITS,
|
||||
GAS_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
|
||||
if source.get("stat_cost") is not None:
|
||||
_async_validate_cost_stat(hass, source["stat_cost"], source_result)
|
||||
@@ -263,15 +299,39 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
)
|
||||
|
||||
elif source["type"] == "solar":
|
||||
_async_validate_energy_stat(hass, source["stat_energy_from"], source_result)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
|
||||
elif source["type"] == "battery":
|
||||
_async_validate_energy_stat(hass, source["stat_energy_from"], source_result)
|
||||
_async_validate_energy_stat(hass, source["stat_energy_to"], source_result)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
source["stat_energy_from"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
source["stat_energy_to"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
source_result,
|
||||
)
|
||||
|
||||
for device in manager.data["device_consumption"]:
|
||||
device_result: list[ValidationIssue] = []
|
||||
result.device_consumption.append(device_result)
|
||||
_async_validate_energy_stat(hass, device["stat_consumption"], device_result)
|
||||
_async_validate_usage_stat(
|
||||
hass,
|
||||
device["stat_consumption"],
|
||||
ENERGY_USAGE_UNITS,
|
||||
ENERGY_UNIT_ERROR,
|
||||
device_result,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -9,6 +9,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
FritzInternalError,
|
||||
FritzServiceError,
|
||||
)
|
||||
from fritzconnection.lib.fritzstatus import FritzStatus
|
||||
@@ -273,7 +274,12 @@ async def async_setup_entry(
|
||||
"GetInfo",
|
||||
)
|
||||
dsl = dslinterface["NewEnable"]
|
||||
except (FritzActionError, FritzActionFailedError, FritzServiceError):
|
||||
except (
|
||||
FritzInternalError,
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzServiceError,
|
||||
):
|
||||
pass
|
||||
|
||||
for sensor_type, sensor_data in SENSOR_DATA.items():
|
||||
|
||||
@@ -2311,16 +2311,12 @@ class SensorStateTrait(_Trait):
|
||||
name = TRAIT_SENSOR_STATE
|
||||
commands = []
|
||||
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class, _):
|
||||
@classmethod
|
||||
def supported(cls, domain, features, device_class, _):
|
||||
"""Test if state is supported."""
|
||||
return domain == sensor.DOMAIN and device_class in (
|
||||
sensor.DEVICE_CLASS_AQI,
|
||||
sensor.DEVICE_CLASS_CO,
|
||||
sensor.DEVICE_CLASS_CO2,
|
||||
sensor.DEVICE_CLASS_PM25,
|
||||
sensor.DEVICE_CLASS_PM10,
|
||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
return (
|
||||
domain == sensor.DOMAIN
|
||||
and device_class in SensorStateTrait.sensor_types.keys()
|
||||
)
|
||||
|
||||
def sync_attributes(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from homeassistant.components.switch import DOMAIN, SwitchEntity
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
from . import ATTR_NEW, CecEntity
|
||||
|
||||
@@ -34,17 +35,25 @@ class CecSwitchEntity(CecEntity, SwitchEntity):
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn device on."""
|
||||
self._device.turn_on()
|
||||
self._attr_is_on = True
|
||||
self._state = STATE_ON
|
||||
self.schedule_update_ha_state(force_refresh=False)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn device off."""
|
||||
self._device.turn_off()
|
||||
self._attr_is_on = False
|
||||
self._state = STATE_OFF
|
||||
self.schedule_update_ha_state(force_refresh=False)
|
||||
|
||||
def toggle(self, **kwargs):
|
||||
"""Toggle the entity."""
|
||||
self._device.toggle()
|
||||
self._attr_is_on = not self._attr_is_on
|
||||
if self._state == STATE_ON:
|
||||
self._state = STATE_OFF
|
||||
else:
|
||||
self._state = STATE_ON
|
||||
self.schedule_update_ha_state(force_refresh=False)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Support for Honeywell (US) Total Connect Comfort climate systems."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import somecomfort
|
||||
@@ -9,7 +10,8 @@ from homeassistant.util import Throttle
|
||||
|
||||
from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||
UPDATE_LOOP_SLEEP_TIME = 5
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
PLATFORMS = ["climate"]
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@ async def async_setup_entry(hass, config):
|
||||
return False
|
||||
|
||||
data = HoneywellData(hass, client, username, password, devices)
|
||||
await data.update()
|
||||
await data.async_update()
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config.entry_id] = data
|
||||
hass.config_entries.async_setup_platforms(config, PLATFORMS)
|
||||
@@ -102,18 +104,19 @@ class HoneywellData:
|
||||
self.devices = devices
|
||||
return True
|
||||
|
||||
def _refresh_devices(self):
|
||||
async def _refresh_devices(self):
|
||||
"""Refresh each enabled device."""
|
||||
for device in self.devices:
|
||||
device.refresh()
|
||||
await self._hass.async_add_executor_job(device.refresh)
|
||||
await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def update(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state."""
|
||||
retries = 3
|
||||
while retries > 0:
|
||||
try:
|
||||
await self._hass.async_add_executor_job(self._refresh_devices)
|
||||
await self._refresh_devices()
|
||||
break
|
||||
except (
|
||||
somecomfort.client.APIRateLimited,
|
||||
@@ -124,7 +127,7 @@ class HoneywellData:
|
||||
if retries == 0:
|
||||
raise exp
|
||||
|
||||
result = await self._hass.async_add_executor_job(self._retry())
|
||||
result = await self._retry()
|
||||
|
||||
if not result:
|
||||
raise exp
|
||||
|
||||
@@ -107,6 +107,8 @@ HW_FAN_MODE_TO_HA = {
|
||||
"follow schedule": FAN_AUTO,
|
||||
}
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Honeywell thermostat."""
|
||||
@@ -384,4 +386,4 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest state from the service."""
|
||||
await self._data.update()
|
||||
await self._data.async_update()
|
||||
|
||||
@@ -118,12 +118,16 @@ async def async_validate_trigger_config(hass, config):
|
||||
|
||||
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||
|
||||
if (
|
||||
not device
|
||||
or device.model not in REMOTES
|
||||
or trigger not in REMOTES[device.model]
|
||||
):
|
||||
raise InvalidDeviceAutomationConfig
|
||||
if not device:
|
||||
raise InvalidDeviceAutomationConfig("Device {config[CONF_DEVICE_ID]} not found")
|
||||
|
||||
if device.model not in REMOTES:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Device model {device.model} is not a remote"
|
||||
)
|
||||
|
||||
if trigger not in REMOTES[device.model]:
|
||||
raise InvalidDeviceAutomationConfig("Device does not support trigger {trigger}")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -67,13 +67,13 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity):
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return max valid temperature that can be set."""
|
||||
return 80.0
|
||||
"""Return min valid temperature that can be set."""
|
||||
return 30.0
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return max valid temperature that can be set."""
|
||||
return 30.0
|
||||
return 80.0
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
|
||||
@@ -106,20 +106,14 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
"""Initialize the integration sensor."""
|
||||
self._sensor_source_id = source_entity
|
||||
self._round_digits = round_digits
|
||||
self._state = 0
|
||||
self._state = STATE_UNAVAILABLE
|
||||
self._method = integration_method
|
||||
|
||||
self._name = name if name is not None else f"{source_entity} integral"
|
||||
|
||||
if unit_of_measurement is None:
|
||||
self._unit_template = (
|
||||
f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}"
|
||||
)
|
||||
# we postpone the definition of unit_of_measurement to later
|
||||
self._unit_of_measurement = None
|
||||
else:
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
self._unit_template = (
|
||||
f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}"
|
||||
)
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._unit_prefix = UNIT_PREFIXES[unit_prefix]
|
||||
self._unit_time = UNIT_TIME[unit_time]
|
||||
self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
|
||||
@@ -135,10 +129,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
_LOGGER.warning("Could not restore last state: %s", err)
|
||||
else:
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
self._unit_of_measurement = state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
if self._unit_of_measurement is None:
|
||||
self._unit_of_measurement = state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
||||
@callback
|
||||
def calc_integration(event):
|
||||
@@ -193,7 +187,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
except AssertionError as err:
|
||||
_LOGGER.error("Could not calculate integral: %s", err)
|
||||
else:
|
||||
self._state += integral
|
||||
if isinstance(self._state, Decimal):
|
||||
self._state += integral
|
||||
else:
|
||||
self._state = integral
|
||||
self.async_write_ha_state()
|
||||
|
||||
async_track_state_change_event(
|
||||
@@ -208,7 +205,9 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return round(self._state, self._round_digits)
|
||||
if isinstance(self._state, Decimal):
|
||||
return round(self._state, self._round_digits)
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LIFX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lifx",
|
||||
"requirements": ["aiolifx==0.6.10", "aiolifx_effects==0.2.2"],
|
||||
"requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"],
|
||||
"homekit": {
|
||||
"models": ["LIFX"]
|
||||
},
|
||||
|
||||
@@ -4,10 +4,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.significant_change import (
|
||||
check_numeric_changed,
|
||||
either_one_none,
|
||||
)
|
||||
from homeassistant.helpers.significant_change import check_absolute_change
|
||||
|
||||
from . import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@@ -37,24 +34,21 @@ def async_check_significant_change(
|
||||
old_color = old_attrs.get(ATTR_HS_COLOR)
|
||||
new_color = new_attrs.get(ATTR_HS_COLOR)
|
||||
|
||||
if either_one_none(old_color, new_color):
|
||||
return True
|
||||
|
||||
if old_color and new_color:
|
||||
# Range 0..360
|
||||
if check_numeric_changed(old_color[0], new_color[0], 5):
|
||||
if check_absolute_change(old_color[0], new_color[0], 5):
|
||||
return True
|
||||
|
||||
# Range 0..100
|
||||
if check_numeric_changed(old_color[1], new_color[1], 3):
|
||||
if check_absolute_change(old_color[1], new_color[1], 3):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3
|
||||
):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
# Default range 153..500
|
||||
old_attrs.get(ATTR_COLOR_TEMP),
|
||||
new_attrs.get(ATTR_COLOR_TEMP),
|
||||
@@ -62,7 +56,7 @@ def async_check_significant_change(
|
||||
):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
# Range 0..255
|
||||
old_attrs.get(ATTR_WHITE_VALUE),
|
||||
new_attrs.get(ATTR_WHITE_VALUE),
|
||||
|
||||
@@ -48,7 +48,7 @@ from homeassistant.helpers.integration_platform import (
|
||||
from homeassistant.loader import bind_hass
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
ENTITY_ID_JSON_TEMPLATE = '"entity_id": ?"{}"'
|
||||
ENTITY_ID_JSON_TEMPLATE = '"entity_id":"{}"'
|
||||
ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"')
|
||||
DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"')
|
||||
ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.2.0"],
|
||||
"requirements": ["pymazda==0.2.1"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -25,9 +25,11 @@ from homeassistant.const import (
|
||||
|
||||
from .const import (
|
||||
CONF_DATA_TYPE,
|
||||
CONF_INPUT_TYPE,
|
||||
CONF_SWAP,
|
||||
CONF_SWAP_BYTE,
|
||||
CONF_SWAP_NONE,
|
||||
CONF_WRITE_TYPE,
|
||||
DATA_TYPE_CUSTOM,
|
||||
DATA_TYPE_FLOAT,
|
||||
DATA_TYPE_FLOAT16,
|
||||
@@ -212,6 +214,10 @@ def duplicate_entity_validator(config: dict) -> dict:
|
||||
for index, entry in enumerate(hub[conf_key]):
|
||||
name = entry[CONF_NAME]
|
||||
addr = str(entry[CONF_ADDRESS])
|
||||
if CONF_INPUT_TYPE in entry:
|
||||
addr += "_" + str(entry[CONF_INPUT_TYPE])
|
||||
elif CONF_WRITE_TYPE in entry:
|
||||
addr += "_" + str(entry[CONF_WRITE_TYPE])
|
||||
if CONF_COMMAND_ON in entry:
|
||||
addr += "_" + str(entry[CONF_COMMAND_ON])
|
||||
if CONF_COMMAND_OFF in entry:
|
||||
@@ -242,7 +248,10 @@ def duplicate_modbus_validator(config: list) -> list:
|
||||
errors = []
|
||||
for index, hub in enumerate(config):
|
||||
name = hub.get(CONF_NAME, DEFAULT_HUB)
|
||||
host = hub[CONF_PORT] if hub[CONF_TYPE] == SERIAL else hub[CONF_HOST]
|
||||
if hub[CONF_TYPE] == SERIAL:
|
||||
host = hub[CONF_PORT]
|
||||
else:
|
||||
host = f"{hub[CONF_HOST]}_{hub[CONF_PORT]}"
|
||||
if host in hosts:
|
||||
err = f"Modbus {name} contains duplicate host/port {host}, not loaded!"
|
||||
_LOGGER.warning(err)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "myq",
|
||||
"name": "MyQ",
|
||||
"documentation": "https://www.home-assistant.io/integrations/myq",
|
||||
"requirements": ["pymyq==3.1.3"],
|
||||
"requirements": ["pymyq==3.1.4"],
|
||||
"codeowners": ["@bdraco","@ehendrix23"],
|
||||
"config_flow": true,
|
||||
"homekit": {
|
||||
|
||||
@@ -54,7 +54,7 @@ async def async_get_type(hass, cloud_id, install_code, host):
|
||||
meters = await hub.get_device_list()
|
||||
except aioeagle.BadAuth as err:
|
||||
raise InvalidAuth from err
|
||||
except aiohttp.ClientError:
|
||||
except (KeyError, aiohttp.ClientError):
|
||||
# This can happen if it's an eagle-100
|
||||
meters = None
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from sqlalchemy import bindparam
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext import baked
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
|
||||
@@ -215,7 +216,14 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool:
|
||||
metadata_id = _update_or_add_metadata(
|
||||
instance.hass, session, entity_id, stat["meta"]
|
||||
)
|
||||
session.add(Statistics.from_stats(metadata_id, start, stat["stat"]))
|
||||
try:
|
||||
session.add(Statistics.from_stats(metadata_id, start, stat["stat"]))
|
||||
except SQLAlchemyError:
|
||||
_LOGGER.exception(
|
||||
"Unexpected exception when inserting statistics %s:%s ",
|
||||
metadata_id,
|
||||
stat,
|
||||
)
|
||||
session.add(StatisticsRuns(start=start))
|
||||
|
||||
return True
|
||||
@@ -369,11 +377,11 @@ def statistics_during_period(
|
||||
)
|
||||
if not stats:
|
||||
return {}
|
||||
return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata)
|
||||
return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata, True)
|
||||
|
||||
|
||||
def get_last_statistics(
|
||||
hass: HomeAssistant, number_of_stats: int, statistic_id: str
|
||||
hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool
|
||||
) -> dict[str, list[dict]]:
|
||||
"""Return the last number_of_stats statistics for a statistic_id."""
|
||||
statistic_ids = [statistic_id]
|
||||
@@ -403,7 +411,9 @@ def get_last_statistics(
|
||||
if not stats:
|
||||
return {}
|
||||
|
||||
return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata)
|
||||
return _sorted_statistics_to_dict(
|
||||
hass, stats, statistic_ids, metadata, convert_units
|
||||
)
|
||||
|
||||
|
||||
def _sorted_statistics_to_dict(
|
||||
@@ -411,11 +421,16 @@ def _sorted_statistics_to_dict(
|
||||
stats: list,
|
||||
statistic_ids: list[str] | None,
|
||||
metadata: dict[str, StatisticMetaData],
|
||||
convert_units: bool,
|
||||
) -> dict[str, list[dict]]:
|
||||
"""Convert SQL results into JSON friendly data structure."""
|
||||
result: dict = defaultdict(list)
|
||||
units = hass.config.units
|
||||
|
||||
def no_conversion(val: Any, _: Any) -> float | None:
|
||||
"""Return x."""
|
||||
return val # type: ignore
|
||||
|
||||
# Set all statistic IDs to empty lists in result set to maintain the order
|
||||
if statistic_ids is not None:
|
||||
for stat_id in statistic_ids:
|
||||
@@ -428,9 +443,11 @@ def _sorted_statistics_to_dict(
|
||||
for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore
|
||||
unit = metadata[meta_id]["unit_of_measurement"]
|
||||
statistic_id = metadata[meta_id]["statistic_id"]
|
||||
convert: Callable[[Any, Any], float | None] = UNIT_CONVERSIONS.get(
|
||||
unit, lambda x, units: x # type: ignore
|
||||
)
|
||||
convert: Callable[[Any, Any], float | None]
|
||||
if convert_units:
|
||||
convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore
|
||||
else:
|
||||
convert = no_conversion
|
||||
ent_results = result[meta_id]
|
||||
ent_results.extend(
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
SENSOR_TYPES = (
|
||||
RfxtrxSensorEntityDescription(
|
||||
key="Barameter",
|
||||
key="Barometer",
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
|
||||
@@ -240,7 +240,8 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
|
||||
|
||||
def _send_key(self, key):
|
||||
"""Send the key using legacy protocol."""
|
||||
self._get_remote().control(key)
|
||||
if remote := self._get_remote():
|
||||
remote.control(key)
|
||||
|
||||
def stop(self):
|
||||
"""Stop Bridge."""
|
||||
@@ -315,7 +316,8 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
||||
"""Send the key using websocket protocol."""
|
||||
if key == "KEY_POWEROFF":
|
||||
key = "KEY_POWER"
|
||||
self._get_remote().send_key(key)
|
||||
if remote := self._get_remote():
|
||||
remote.send_key(key)
|
||||
|
||||
def _get_remote(self, avoid_open: bool = False):
|
||||
"""Create or return a remote control instance."""
|
||||
|
||||
@@ -209,6 +209,9 @@ class SensorEntity(Entity):
|
||||
and not self._last_reset_reported
|
||||
):
|
||||
self._last_reset_reported = True
|
||||
if self.platform and self.platform.platform_name == "energy":
|
||||
return {ATTR_LAST_RESET: last_reset.isoformat()}
|
||||
|
||||
report_issue = self._suggest_report_issue()
|
||||
_LOGGER.warning(
|
||||
"Entity %s (%s) with state_class %s has set last_reset. Setting "
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.recorder import history, statistics
|
||||
@@ -129,13 +130,6 @@ def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str | None]]:
|
||||
return entity_ids
|
||||
|
||||
|
||||
# Faster than try/except
|
||||
# From https://stackoverflow.com/a/23639915
|
||||
def _is_number(s: str) -> bool: # pylint: disable=invalid-name
|
||||
"""Return True if string is a number."""
|
||||
return s.replace(".", "", 1).isdigit()
|
||||
|
||||
|
||||
def _time_weighted_average(
|
||||
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
|
||||
) -> float:
|
||||
@@ -179,6 +173,14 @@ def _get_units(fstates: list[tuple[float, State]]) -> set[str | None]:
|
||||
return {item[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) for item in fstates}
|
||||
|
||||
|
||||
def _parse_float(state: str) -> float:
|
||||
"""Parse a float string, throw on inf or nan."""
|
||||
fstate = float(state)
|
||||
if math.isnan(fstate) or math.isinf(fstate):
|
||||
raise ValueError
|
||||
return fstate
|
||||
|
||||
|
||||
def _normalize_states(
|
||||
hass: HomeAssistant,
|
||||
entity_history: list[State],
|
||||
@@ -190,9 +192,14 @@ def _normalize_states(
|
||||
|
||||
if device_class not in UNIT_CONVERSIONS:
|
||||
# We're not normalizing this device class, return the state as they are
|
||||
fstates = [
|
||||
(float(el.state), el) for el in entity_history if _is_number(el.state)
|
||||
]
|
||||
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:
|
||||
@@ -220,11 +227,10 @@ def _normalize_states(
|
||||
fstates = []
|
||||
|
||||
for state in entity_history:
|
||||
# Exclude non numerical states from statistics
|
||||
if not _is_number(state.state):
|
||||
try:
|
||||
fstate = _parse_float(state.state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
fstate = float(state.state)
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
# Exclude unsupported units from statistics
|
||||
if unit not in UNIT_CONVERSIONS[device_class]:
|
||||
@@ -384,7 +390,7 @@ def compile_statistics( # noqa: C901
|
||||
last_reset = old_last_reset = None
|
||||
new_state = old_state = None
|
||||
_sum = 0
|
||||
last_stats = statistics.get_last_statistics(hass, 1, entity_id)
|
||||
last_stats = statistics.get_last_statistics(hass, 1, entity_id, False)
|
||||
if entity_id in last_stats:
|
||||
# We have compiled history for this sensor before, use that as a starting point
|
||||
last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"]
|
||||
@@ -435,8 +441,8 @@ def compile_statistics( # noqa: C901
|
||||
_LOGGER.info(
|
||||
"Detected new cycle for %s, value dropped from %s to %s",
|
||||
entity_id,
|
||||
fstate,
|
||||
new_state,
|
||||
fstate,
|
||||
)
|
||||
|
||||
if reset:
|
||||
|
||||
@@ -9,8 +9,33 @@ from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.significant_change import (
|
||||
check_absolute_change,
|
||||
check_percentage_change,
|
||||
)
|
||||
|
||||
from . import DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE
|
||||
from . import (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CO,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
)
|
||||
|
||||
|
||||
def _absolute_and_relative_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
absolute_change: int | float,
|
||||
percentage_change: int | float,
|
||||
) -> bool:
|
||||
return check_absolute_change(
|
||||
old_state, new_state, absolute_change
|
||||
) and check_percentage_change(old_state, new_state, percentage_change)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -28,20 +53,35 @@ def async_check_significant_change(
|
||||
if device_class is None:
|
||||
return None
|
||||
|
||||
absolute_change: float | None = None
|
||||
percentage_change: float | None = None
|
||||
if device_class == DEVICE_CLASS_TEMPERATURE:
|
||||
if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT:
|
||||
change: float | int = 1
|
||||
absolute_change = 1.0
|
||||
else:
|
||||
change = 0.5
|
||||
|
||||
old_value = float(old_state)
|
||||
new_value = float(new_state)
|
||||
return abs(old_value - new_value) >= change
|
||||
absolute_change = 0.5
|
||||
|
||||
if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY):
|
||||
old_value = float(old_state)
|
||||
new_value = float(new_state)
|
||||
absolute_change = 1.0
|
||||
|
||||
return abs(old_value - new_value) >= 1
|
||||
if device_class in (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CO,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
):
|
||||
absolute_change = 1.0
|
||||
percentage_change = 2.0
|
||||
|
||||
if absolute_change is not None and percentage_change is not None:
|
||||
return _absolute_and_relative_change(
|
||||
float(old_state), float(new_state), absolute_change, percentage_change
|
||||
)
|
||||
if absolute_change is not None:
|
||||
return check_absolute_change(
|
||||
float(old_state), float(new_state), absolute_change
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -32,6 +32,8 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
# Deprecated in Home Assistant 2021.6
|
||||
@@ -46,8 +48,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
): cv.positive_time_period,
|
||||
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)
|
||||
): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]),
|
||||
CONF_MONITORED_CONDITIONS, default=list(SENSOR_KEYS)
|
||||
): vol.All(cv.ensure_list, [vol.In(list(SENSOR_KEYS))]),
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -289,7 +289,7 @@ class Scanner:
|
||||
def _async_unsee(self, header_st: str | None, header_location: str | None) -> None:
|
||||
"""If we see a device in a new location, unsee the original location."""
|
||||
if header_st is not None:
|
||||
self.seen.remove((header_st, header_location))
|
||||
self.seen.discard((header_st, header_location))
|
||||
|
||||
async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
|
||||
"""Process SSDP entries."""
|
||||
|
||||
@@ -90,7 +90,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
sensor
|
||||
for device in account.api.devices.values()
|
||||
for description in SENSOR_TYPES
|
||||
if (sensor := StarlineSensor(account, device, description)).state is not None
|
||||
if (sensor := StarlineSensor(account, device, description)).native_value
|
||||
is not None
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class SurePetcareAPI:
|
||||
"""Get the latest data from Sure Petcare."""
|
||||
|
||||
try:
|
||||
self.states = await self.surepy.get_entities()
|
||||
self.states = await self.surepy.get_entities(refresh=True)
|
||||
except SurePetcareError as error:
|
||||
_LOGGER.error("Unable to fetch data: %s", error)
|
||||
return
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Switcher",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
|
||||
"codeowners": ["@tomerfi","@thecode"],
|
||||
"requirements": ["aioswitcher==2.0.4"],
|
||||
"requirements": ["aioswitcher==2.0.5"],
|
||||
"iot_class": "local_push",
|
||||
"config_flow": true
|
||||
}
|
||||
|
||||
@@ -46,27 +46,27 @@ SELECT_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
async def _async_create_entities(
|
||||
hass: HomeAssistant, entities: list[dict[str, Any]], unique_id_prefix: str | None
|
||||
hass: HomeAssistant, definitions: list[dict[str, Any]], unique_id_prefix: str | None
|
||||
) -> list[TemplateSelect]:
|
||||
"""Create the Template select."""
|
||||
for entity in entities:
|
||||
unique_id = entity.get(CONF_UNIQUE_ID)
|
||||
|
||||
entities = []
|
||||
for definition in definitions:
|
||||
unique_id = definition.get(CONF_UNIQUE_ID)
|
||||
if unique_id and unique_id_prefix:
|
||||
unique_id = f"{unique_id_prefix}-{unique_id}"
|
||||
|
||||
return [
|
||||
entities.append(
|
||||
TemplateSelect(
|
||||
hass,
|
||||
entity.get(CONF_NAME, DEFAULT_NAME),
|
||||
entity[CONF_STATE],
|
||||
entity.get(CONF_AVAILABILITY),
|
||||
entity[CONF_SELECT_OPTION],
|
||||
entity[ATTR_OPTIONS],
|
||||
entity.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC),
|
||||
definition.get(CONF_NAME, DEFAULT_NAME),
|
||||
definition[CONF_STATE],
|
||||
definition.get(CONF_AVAILABILITY),
|
||||
definition[CONF_SELECT_OPTION],
|
||||
definition[ATTR_OPTIONS],
|
||||
definition.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC),
|
||||
unique_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
|
||||
@@ -69,7 +69,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
|
||||
# We make a copy so our initial render is 'unknown' and not 'unavailable'
|
||||
self._rendered = dict(self._static_rendered)
|
||||
self._parse_result = set()
|
||||
self._parse_result = {CONF_AVAILABILITY}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -80,7 +80,7 @@ class ThinkingCleanerSwitch(SwitchEntity):
|
||||
self.last_lock_time = None
|
||||
self.graceful_state = False
|
||||
|
||||
self._attr_name = f"{tc_object} {description.name}"
|
||||
self._attr_name = f"{tc_object.name} {description.name}"
|
||||
|
||||
def lock_update(self):
|
||||
"""Lock the update since TC clean takes some time to update."""
|
||||
|
||||
@@ -112,7 +112,7 @@ class USBDiscovery:
|
||||
if not sys.platform.startswith("linux"):
|
||||
return
|
||||
info = await system_info.async_get_system_info(self.hass)
|
||||
if info.get("docker") and not info.get("hassio"):
|
||||
if info.get("docker"):
|
||||
return
|
||||
|
||||
from pyudev import ( # pylint: disable=import-outside-toplevel
|
||||
|
||||
@@ -116,6 +116,13 @@ MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1"
|
||||
MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1"
|
||||
MODEL_AIRQUALITYMONITOR_CGDN1 = "cgllc.airm.cgdn1"
|
||||
|
||||
MODELS_AIR_QUALITY_MONITOR = [
|
||||
MODEL_AIRQUALITYMONITOR_V1,
|
||||
MODEL_AIRQUALITYMONITOR_B1,
|
||||
MODEL_AIRQUALITYMONITOR_S1,
|
||||
MODEL_AIRQUALITYMONITOR_CGDN1,
|
||||
]
|
||||
|
||||
# Light Models
|
||||
MODELS_LIGHT_EYECARE = ["philips.light.sread1"]
|
||||
MODELS_LIGHT_CEILING = ["philips.light.ceiling", "philips.light.zyceiling"]
|
||||
|
||||
@@ -301,7 +301,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true when state is known."""
|
||||
return self._available
|
||||
return super().available and self._available
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
@@ -405,36 +405,42 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._speed_count = 1
|
||||
self._operation_mode_class = AirpurifierOperationMode
|
||||
elif self._model == MODEL_AIRPURIFIER_PRO_V7:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._speed_count = 1
|
||||
self._operation_mode_class = AirpurifierOperationMode
|
||||
elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER_2S
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._speed_count = 1
|
||||
self._operation_mode_class = AirpurifierOperationMode
|
||||
elif self._model in MODELS_PURIFIER_MIOT:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_MIOT
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER_MIOT
|
||||
self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
|
||||
self._speed_count = 3
|
||||
self._operation_mode_class = AirpurifierMiotOperationMode
|
||||
elif self._model == MODEL_AIRPURIFIER_V3:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER_V3
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._speed_count = 1
|
||||
self._operation_mode_class = AirpurifierOperationMode
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_MIIO
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
|
||||
self._preset_modes = PRESET_MODES_AIRPURIFIER
|
||||
self._supported_features = SUPPORT_PRESET_MODE
|
||||
self._speed_count = 1
|
||||
self._operation_mode_class = AirpurifierOperationMode
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes}
|
||||
@@ -446,7 +452,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
def preset_mode(self):
|
||||
"""Get the active preset mode."""
|
||||
if self._state:
|
||||
preset_mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name
|
||||
preset_mode = self._operation_mode_class(self._mode).name
|
||||
return preset_mode if preset_mode in self._preset_modes else None
|
||||
|
||||
return None
|
||||
@@ -455,7 +461,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
def percentage(self):
|
||||
"""Return the current percentage based speed."""
|
||||
if self._state:
|
||||
mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE])
|
||||
mode = self._operation_mode_class(self._state_attrs[ATTR_MODE])
|
||||
if mode in self.REVERSE_SPEED_MODE_MAPPING:
|
||||
return ranged_value_to_percentage(
|
||||
(1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode]
|
||||
@@ -479,7 +485,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
AirpurifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]),
|
||||
self._operation_mode_class(self.SPEED_MODE_MAPPING[speed_mode]),
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
@@ -490,11 +496,13 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
await self._try_command(
|
||||
if await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode,
|
||||
self.PRESET_MODE_MAPPING[preset_mode],
|
||||
)
|
||||
):
|
||||
self._mode = self._operation_mode_class[preset_mode].value
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_extra_features(self, features: int = 1):
|
||||
"""Set the extra features."""
|
||||
@@ -538,15 +546,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Get the active preset mode."""
|
||||
if self._state:
|
||||
preset_mode = AirpurifierMiotOperationMode(self._mode).name
|
||||
return preset_mode if preset_mode in self._preset_modes else None
|
||||
|
||||
return None
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan.
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity):
|
||||
self._available_modes = AVAILABLE_MODES_MJJSQ
|
||||
self._min_humidity = 30
|
||||
self._max_humidity = 80
|
||||
self._humidity_steps = 10
|
||||
self._humidity_steps = 100
|
||||
else:
|
||||
self._available_modes = AVAILABLE_MODES_OTHER
|
||||
self._min_humidity = 30
|
||||
|
||||
@@ -66,6 +66,7 @@ from .const import (
|
||||
MODEL_FAN_ZA1,
|
||||
MODEL_FAN_ZA3,
|
||||
MODEL_FAN_ZA4,
|
||||
MODELS_AIR_QUALITY_MONITOR,
|
||||
MODELS_HUMIDIFIER_MIIO,
|
||||
MODELS_HUMIDIFIER_MIOT,
|
||||
MODELS_HUMIDIFIER_MJJSQ,
|
||||
@@ -371,23 +372,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
host = config_entry.data[CONF_HOST]
|
||||
token = config_entry.data[CONF_TOKEN]
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE)
|
||||
sensors = []
|
||||
|
||||
if model in (MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5):
|
||||
return
|
||||
if model in MODEL_TO_SENSORS_MAP:
|
||||
sensors = MODEL_TO_SENSORS_MAP[model]
|
||||
elif model in MODELS_HUMIDIFIER_MIOT:
|
||||
sensors = HUMIDIFIER_MIOT_SENSORS
|
||||
elif model in MODELS_HUMIDIFIER_MJJSQ:
|
||||
sensors = HUMIDIFIER_MJJSQ_SENSORS
|
||||
elif model in MODELS_HUMIDIFIER_MIIO:
|
||||
sensors = HUMIDIFIER_MIIO_SENSORS
|
||||
elif model in MODELS_PURIFIER_MIIO:
|
||||
sensors = PURIFIER_MIIO_SENSORS
|
||||
elif model in MODELS_PURIFIER_MIOT:
|
||||
sensors = PURIFIER_MIOT_SENSORS
|
||||
else:
|
||||
|
||||
if model in MODELS_AIR_QUALITY_MONITOR:
|
||||
unique_id = config_entry.unique_id
|
||||
name = config_entry.title
|
||||
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
|
||||
@@ -399,19 +388,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
name, device, config_entry, unique_id, description
|
||||
)
|
||||
)
|
||||
for sensor, description in SENSOR_TYPES.items():
|
||||
if sensor not in sensors:
|
||||
continue
|
||||
entities.append(
|
||||
XiaomiGenericSensor(
|
||||
f"{config_entry.title} {description.name}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{sensor}_{config_entry.unique_id}",
|
||||
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR],
|
||||
description,
|
||||
else:
|
||||
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
|
||||
sensors = []
|
||||
if model in MODEL_TO_SENSORS_MAP:
|
||||
sensors = MODEL_TO_SENSORS_MAP[model]
|
||||
elif model in MODELS_HUMIDIFIER_MIOT:
|
||||
sensors = HUMIDIFIER_MIOT_SENSORS
|
||||
elif model in MODELS_HUMIDIFIER_MJJSQ:
|
||||
sensors = HUMIDIFIER_MJJSQ_SENSORS
|
||||
elif model in MODELS_HUMIDIFIER_MIIO:
|
||||
sensors = HUMIDIFIER_MIIO_SENSORS
|
||||
elif model in MODELS_PURIFIER_MIIO:
|
||||
sensors = PURIFIER_MIIO_SENSORS
|
||||
elif model in MODELS_PURIFIER_MIOT:
|
||||
sensors = PURIFIER_MIOT_SENSORS
|
||||
|
||||
for sensor, description in SENSOR_TYPES.items():
|
||||
if sensor not in sensors:
|
||||
continue
|
||||
entities.append(
|
||||
XiaomiGenericSensor(
|
||||
f"{config_entry.title} {description.name}",
|
||||
device,
|
||||
config_entry,
|
||||
f"{sensor}_{config_entry.unique_id}",
|
||||
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR],
|
||||
description,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast",
|
||||
"requirements": [
|
||||
"aiomusiccast==0.9.1"
|
||||
"aiomusiccast==0.9.2"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_DEVICE: {
|
||||
**current_entry.data[CONF_DEVICE],
|
||||
**current_entry.data.get(CONF_DEVICE, {}),
|
||||
CONF_DEVICE_PATH: dev_path,
|
||||
},
|
||||
}
|
||||
@@ -172,7 +172,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_DEVICE: {
|
||||
**current_entry.data[CONF_DEVICE],
|
||||
**current_entry.data.get(CONF_DEVICE, {}),
|
||||
CONF_DEVICE_PATH: device_path,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -326,11 +326,6 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
|
||||
device = discovery_info["device"]
|
||||
manufacturer = discovery_info["manufacturer"]
|
||||
description = discovery_info["description"]
|
||||
# The Nortek sticks are a special case since they
|
||||
# have a Z-Wave and a Zigbee radio. We need to reject
|
||||
# the Zigbee radio.
|
||||
if vid == "10C4" and pid == "8A2A" and "Z-Wave" not in description:
|
||||
return self.async_abort(reason="not_zwave_device")
|
||||
# Zooz uses this vid/pid, but so do 2652 sticks
|
||||
if vid == "10C4" and pid == "EA60" and "2652" in description:
|
||||
return self.async_abort(reason="not_zwave_device")
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const.command_class.barrior_operator import BarrierState
|
||||
from zwave_js_server.const.command_class.barrier_operator import BarrierState
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
|
||||
@@ -32,8 +32,8 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
)
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
|
||||
from zwave_js_server.util.command_class import (
|
||||
get_meter_scale_type,
|
||||
from zwave_js_server.util.command_class.meter import get_meter_scale_type
|
||||
from zwave_js_server.util.command_class.multilevel_sensor import (
|
||||
get_multilevel_sensor_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.29.1"],
|
||||
"requirements": ["zwave-js-server-python==0.30.0"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["usb", "http", "websocket_api"],
|
||||
"iot_class": "local_push",
|
||||
"usb": [
|
||||
{"vid":"0658","pid":"0200","known_devices":["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"]},
|
||||
{"vid":"10C4","pid":"8A2A","known_devices":["Nortek HUSBZB-1"]},
|
||||
{"vid":"10C4","pid":"8A2A","description":"*z-wave*","known_devices":["Nortek HUSBZB-1"]},
|
||||
{"vid":"10C4","pid":"EA60","known_devices":["Aeotec Z-Stick 7", "Silicon Labs UZB-7", "Zooz ZST10 700"]}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ from zwave_js_server.const.command_class.meter import (
|
||||
)
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import ConfigurationValue
|
||||
from zwave_js_server.util.command_class import get_meter_type
|
||||
from zwave_js_server.util.command_class.meter import get_meter_type
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_ENERGY,
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const.command_class.barrior_operator import (
|
||||
from zwave_js_server.const.command_class.barrier_operator import (
|
||||
BarrierEventSignalingSubsystemState,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "6"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
||||
@@ -32,7 +32,8 @@ USB = [
|
||||
{
|
||||
"domain": "zwave_js",
|
||||
"vid": "10C4",
|
||||
"pid": "8A2A"
|
||||
"pid": "8A2A",
|
||||
"description": "*z-wave*"
|
||||
},
|
||||
{
|
||||
"domain": "zwave_js",
|
||||
|
||||
@@ -6,16 +6,10 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import (
|
||||
CoreState,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, State, callback, valid_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers import entity_registry, start
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
@@ -63,42 +57,36 @@ class StoredState:
|
||||
class RestoreStateData:
|
||||
"""Helper class for managing the helper saved data."""
|
||||
|
||||
@classmethod
|
||||
async def async_get_instance(cls, hass: HomeAssistant) -> RestoreStateData:
|
||||
@staticmethod
|
||||
@singleton(DATA_RESTORE_STATE_TASK)
|
||||
async def async_get_instance(hass: HomeAssistant) -> RestoreStateData:
|
||||
"""Get the singleton instance of this data helper."""
|
||||
data = RestoreStateData(hass)
|
||||
|
||||
@singleton(DATA_RESTORE_STATE_TASK)
|
||||
async def load_instance(hass: HomeAssistant) -> RestoreStateData:
|
||||
"""Get the singleton instance of this data helper."""
|
||||
data = cls(hass)
|
||||
try:
|
||||
stored_states = await data.store.async_load()
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error loading last states", exc_info=exc)
|
||||
stored_states = None
|
||||
|
||||
try:
|
||||
stored_states = await data.store.async_load()
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error loading last states", exc_info=exc)
|
||||
stored_states = None
|
||||
if stored_states is None:
|
||||
_LOGGER.debug("Not creating cache - no saved states found")
|
||||
data.last_states = {}
|
||||
else:
|
||||
data.last_states = {
|
||||
item["state"]["entity_id"]: StoredState.from_dict(item)
|
||||
for item in stored_states
|
||||
if valid_entity_id(item["state"]["entity_id"])
|
||||
}
|
||||
_LOGGER.debug("Created cache with %s", list(data.last_states))
|
||||
|
||||
if stored_states is None:
|
||||
_LOGGER.debug("Not creating cache - no saved states found")
|
||||
data.last_states = {}
|
||||
else:
|
||||
data.last_states = {
|
||||
item["state"]["entity_id"]: StoredState.from_dict(item)
|
||||
for item in stored_states
|
||||
if valid_entity_id(item["state"]["entity_id"])
|
||||
}
|
||||
_LOGGER.debug("Created cache with %s", list(data.last_states))
|
||||
async def hass_start(hass: HomeAssistant) -> None:
|
||||
"""Start the restore state task."""
|
||||
data.async_setup_dump()
|
||||
|
||||
if hass.state == CoreState.running:
|
||||
data.async_setup_dump()
|
||||
else:
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, data.async_setup_dump
|
||||
)
|
||||
start.async_at_start(hass, hass_start)
|
||||
|
||||
return data
|
||||
|
||||
return cast(RestoreStateData, await load_instance(hass))
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
async def async_save_persistent_states(cls, hass: HomeAssistant) -> None:
|
||||
@@ -269,7 +257,9 @@ class RestoreEntity(Entity):
|
||||
# Return None if this entity isn't added to hass yet
|
||||
_LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable]
|
||||
return None
|
||||
data = await RestoreStateData.async_get_instance(self.hass)
|
||||
data = cast(
|
||||
RestoreStateData, await RestoreStateData.async_get_instance(self.hass)
|
||||
)
|
||||
if self.entity_id not in data.last_states:
|
||||
return None
|
||||
return data.last_states[self.entity_id].state
|
||||
|
||||
@@ -95,25 +95,55 @@ def either_one_none(val1: Any | None, val2: Any | None) -> bool:
|
||||
return (val1 is None and val2 is not None) or (val1 is not None and val2 is None)
|
||||
|
||||
|
||||
def check_numeric_changed(
|
||||
def _check_numeric_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
change: int | float,
|
||||
metric: Callable[[int | float, int | float], int | float],
|
||||
) -> bool:
|
||||
"""Check if two numeric values have changed."""
|
||||
if old_state is None and new_state is None:
|
||||
return False
|
||||
|
||||
if either_one_none(old_state, new_state):
|
||||
return True
|
||||
|
||||
assert old_state is not None
|
||||
assert new_state is not None
|
||||
|
||||
if metric(old_state, new_state) >= change:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_absolute_change(
|
||||
val1: int | float | None,
|
||||
val2: int | float | None,
|
||||
change: int | float,
|
||||
) -> bool:
|
||||
"""Check if two numeric values have changed."""
|
||||
if val1 is None and val2 is None:
|
||||
return False
|
||||
return _check_numeric_change(
|
||||
val1, val2, change, lambda val1, val2: abs(val1 - val2)
|
||||
)
|
||||
|
||||
if either_one_none(val1, val2):
|
||||
return True
|
||||
|
||||
assert val1 is not None
|
||||
assert val2 is not None
|
||||
def check_percentage_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
change: int | float,
|
||||
) -> bool:
|
||||
"""Check if two numeric values have changed."""
|
||||
|
||||
if abs(val1 - val2) >= change:
|
||||
return True
|
||||
def percentage_change(old_state: int | float, new_state: int | float) -> float:
|
||||
if old_state == new_state:
|
||||
return 0
|
||||
try:
|
||||
return (abs(new_state - old_state) / old_state) * 100.0
|
||||
except ZeroDivisionError:
|
||||
return float("inf")
|
||||
|
||||
return False
|
||||
return _check_numeric_change(old_state, new_state, change, percentage_change)
|
||||
|
||||
|
||||
class SignificantlyChangedChecker:
|
||||
|
||||
@@ -26,31 +26,27 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]:
|
||||
@bind_hass
|
||||
@functools.wraps(func)
|
||||
def wrapped(hass: HomeAssistant) -> T:
|
||||
obj: T | None = hass.data.get(data_key)
|
||||
if obj is None:
|
||||
obj = hass.data[data_key] = func(hass)
|
||||
return obj
|
||||
if data_key not in hass.data:
|
||||
hass.data[data_key] = func(hass)
|
||||
return cast(T, hass.data[data_key])
|
||||
|
||||
return wrapped
|
||||
|
||||
@bind_hass
|
||||
@functools.wraps(func)
|
||||
async def async_wrapped(hass: HomeAssistant) -> T:
|
||||
obj_or_evt = hass.data.get(data_key)
|
||||
|
||||
if not obj_or_evt:
|
||||
if data_key not in hass.data:
|
||||
evt = hass.data[data_key] = asyncio.Event()
|
||||
|
||||
result = await func(hass)
|
||||
|
||||
hass.data[data_key] = result
|
||||
evt.set()
|
||||
return cast(T, result)
|
||||
|
||||
obj_or_evt = hass.data[data_key]
|
||||
|
||||
if isinstance(obj_or_evt, asyncio.Event):
|
||||
evt = obj_or_evt
|
||||
await evt.wait()
|
||||
return cast(T, hass.data.get(data_key))
|
||||
await obj_or_evt.wait()
|
||||
return cast(T, hass.data[data_key])
|
||||
|
||||
return cast(T, obj_or_evt)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from unittest.mock import patch
|
||||
from homeassistant import core
|
||||
from homeassistant.config import get_default_config_dir
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import area_registry, device_registry, entity_registry
|
||||
from homeassistant.helpers.check_config import async_check_ha_config_file
|
||||
from homeassistant.util.yaml import Secrets
|
||||
import homeassistant.util.yaml.loader as yaml_loader
|
||||
@@ -229,6 +230,9 @@ async def async_check_config(config_dir):
|
||||
"""Check the HA config."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = config_dir
|
||||
await area_registry.async_load(hass)
|
||||
await device_registry.async_load(hass)
|
||||
await entity_registry.async_load(hass)
|
||||
components = await async_check_ha_config_file(hass)
|
||||
await hass.async_stop(force=True)
|
||||
return components
|
||||
|
||||
@@ -201,7 +201,7 @@ aiokafka==0.6.0
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.6.10
|
||||
aiolifx==0.7.0
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.2.2
|
||||
@@ -216,7 +216,7 @@ aiolyric==1.0.7
|
||||
aiomodernforms==0.1.8
|
||||
|
||||
# homeassistant.components.yamaha_musiccast
|
||||
aiomusiccast==0.9.1
|
||||
aiomusiccast==0.9.2
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
aionotify==0.2.0
|
||||
@@ -243,7 +243,7 @@ aiorecollect==1.0.8
|
||||
aioshelly==0.6.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==2.0.4
|
||||
aioswitcher==2.0.5
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@@ -276,7 +276,7 @@ ambee==0.3.0
|
||||
ambiclimate==0.2.1
|
||||
|
||||
# homeassistant.components.amcrest
|
||||
amcrest==1.8.0
|
||||
amcrest==1.8.1
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.60
|
||||
@@ -1599,7 +1599,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.2.0
|
||||
pymazda==0.2.1
|
||||
|
||||
# homeassistant.components.mediaroom
|
||||
pymediaroom==0.6.4.1
|
||||
@@ -1629,7 +1629,7 @@ pymonoprice==0.3
|
||||
pymsteams==0.1.12
|
||||
|
||||
# homeassistant.components.myq
|
||||
pymyq==3.1.3
|
||||
pymyq==3.1.4
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.21.0
|
||||
@@ -2489,4 +2489,4 @@ zigpy==0.37.1
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.29.1
|
||||
zwave-js-server-python==0.30.0
|
||||
|
||||
@@ -140,7 +140,7 @@ aiolyric==1.0.7
|
||||
aiomodernforms==0.1.8
|
||||
|
||||
# homeassistant.components.yamaha_musiccast
|
||||
aiomusiccast==0.9.1
|
||||
aiomusiccast==0.9.2
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==3.0.2
|
||||
@@ -164,7 +164,7 @@ aiorecollect==1.0.8
|
||||
aioshelly==0.6.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==2.0.4
|
||||
aioswitcher==2.0.5
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@@ -921,7 +921,7 @@ pymailgunner==1.4
|
||||
pymata-express==1.19
|
||||
|
||||
# homeassistant.components.mazda
|
||||
pymazda==0.2.0
|
||||
pymazda==0.2.1
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
pymelcloud==2.5.3
|
||||
@@ -942,7 +942,7 @@ pymodbus==2.5.3rc1
|
||||
pymonoprice==0.3
|
||||
|
||||
# homeassistant.components.myq
|
||||
pymyq==3.1.3
|
||||
pymyq==3.1.4
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.21.0
|
||||
@@ -1400,4 +1400,4 @@ zigpy-znp==0.5.4
|
||||
zigpy==0.37.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.29.1
|
||||
zwave-js-server-python==0.30.0
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""The test for light device automation."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import device_automation
|
||||
@@ -443,6 +445,28 @@ async def test_async_get_device_automations_all_devices_action(
|
||||
assert len(result[device_entry.id]) == 3
|
||||
|
||||
|
||||
async def test_async_get_device_automations_all_devices_action_exception_throw(
|
||||
hass, device_reg, entity_reg, caplog
|
||||
):
|
||||
"""Test we get can fetch all the actions when no device id is passed and can handle one throwing an exception."""
|
||||
await async_setup_component(hass, "device_automation", {})
|
||||
config_entry = MockConfigEntry(domain="test", data={})
|
||||
config_entry.add_to_hass(hass)
|
||||
device_entry = device_reg.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
|
||||
with patch(
|
||||
"homeassistant.components.light.device_trigger.async_get_triggers",
|
||||
side_effect=KeyError,
|
||||
):
|
||||
result = await device_automation.async_get_device_automations(hass, "trigger")
|
||||
assert device_entry.id in result
|
||||
assert len(result[device_entry.id]) == 0
|
||||
assert "KeyError" in caplog.text
|
||||
|
||||
|
||||
async def test_websocket_get_trigger_capabilities(
|
||||
hass, hass_ws_client, device_reg, entity_reg
|
||||
):
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_cost_sensor_no_states(hass, hass_storage) -> None:
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_cost_sensor_price_entity(
|
||||
async def test_cost_sensor_price_entity_total_increasing(
|
||||
hass,
|
||||
hass_storage,
|
||||
hass_ws_client,
|
||||
@@ -89,7 +89,7 @@ async def test_cost_sensor_price_entity(
|
||||
cost_sensor_entity_id,
|
||||
flow_type,
|
||||
) -> None:
|
||||
"""Test energy cost price from sensor entity."""
|
||||
"""Test energy cost price from total_increasing type sensor entity."""
|
||||
|
||||
def _compile_statistics(_):
|
||||
return compile_statistics(hass, now, now + timedelta(seconds=1))
|
||||
@@ -136,6 +136,7 @@ async def test_cost_sensor_price_entity(
|
||||
}
|
||||
|
||||
now = dt_util.utcnow()
|
||||
last_reset_cost_sensor = now.isoformat()
|
||||
|
||||
# Optionally initialize dependent entities
|
||||
if initial_energy is not None:
|
||||
@@ -152,7 +153,9 @@ async def test_cost_sensor_price_entity(
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == initial_cost
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
if initial_cost != "unknown":
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||
|
||||
# Optional late setup of dependent entities
|
||||
@@ -168,7 +171,8 @@ async def test_cost_sensor_price_entity(
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "0.0"
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||
|
||||
# # Unique ID temp disabled
|
||||
@@ -185,6 +189,7 @@ async def test_cost_sensor_price_entity(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Nothing happens when price changes
|
||||
if price_entity is not None:
|
||||
@@ -199,6 +204,7 @@ async def test_cost_sensor_price_entity(
|
||||
assert msg["success"]
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Additional consumption is using the new price
|
||||
hass.states.async_set(
|
||||
@@ -209,6 +215,7 @@ async def test_cost_sensor_price_entity(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Check generated statistics
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
@@ -225,6 +232,7 @@ async def test_cost_sensor_price_entity(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
||||
hass.states.async_set(
|
||||
@@ -235,6 +243,8 @@ async def test_cost_sensor_price_entity(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR
|
||||
assert state.attributes["last_reset"] != last_reset_cost_sensor
|
||||
last_reset_cost_sensor = state.attributes["last_reset"]
|
||||
|
||||
# Energy use bumped to 10 kWh
|
||||
hass.states.async_set(
|
||||
@@ -245,6 +255,213 @@ async def test_cost_sensor_price_entity(
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Check generated statistics
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
||||
assert cost_sensor_entity_id in statistics
|
||||
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 38.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")])
|
||||
@pytest.mark.parametrize(
|
||||
"price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"usage_sensor_entity_id,cost_sensor_entity_id,flow_type",
|
||||
[
|
||||
("sensor.energy_consumption", "sensor.energy_consumption_cost", "flow_from"),
|
||||
(
|
||||
"sensor.energy_production",
|
||||
"sensor.energy_production_compensation",
|
||||
"flow_to",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("energy_state_class", ["measurement"])
|
||||
async def test_cost_sensor_price_entity_total(
|
||||
hass,
|
||||
hass_storage,
|
||||
hass_ws_client,
|
||||
initial_energy,
|
||||
initial_cost,
|
||||
price_entity,
|
||||
fixed_price,
|
||||
usage_sensor_entity_id,
|
||||
cost_sensor_entity_id,
|
||||
flow_type,
|
||||
energy_state_class,
|
||||
) -> None:
|
||||
"""Test energy cost price from total type sensor entity."""
|
||||
|
||||
def _compile_statistics(_):
|
||||
return compile_statistics(hass, now, now + timedelta(seconds=1))
|
||||
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||
ATTR_STATE_CLASS: energy_state_class,
|
||||
}
|
||||
|
||||
await async_init_recorder_component(hass)
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
energy_data["energy_sources"].append(
|
||||
{
|
||||
"type": "grid",
|
||||
"flow_from": [
|
||||
{
|
||||
"stat_energy_from": "sensor.energy_consumption",
|
||||
"entity_energy_from": "sensor.energy_consumption",
|
||||
"stat_cost": None,
|
||||
"entity_energy_price": price_entity,
|
||||
"number_energy_price": fixed_price,
|
||||
}
|
||||
]
|
||||
if flow_type == "flow_from"
|
||||
else [],
|
||||
"flow_to": [
|
||||
{
|
||||
"stat_energy_to": "sensor.energy_production",
|
||||
"entity_energy_to": "sensor.energy_production",
|
||||
"stat_compensation": None,
|
||||
"entity_energy_price": price_entity,
|
||||
"number_energy_price": fixed_price,
|
||||
}
|
||||
]
|
||||
if flow_type == "flow_to"
|
||||
else [],
|
||||
"cost_adjustment_day": 0,
|
||||
}
|
||||
)
|
||||
|
||||
hass_storage[data.STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": energy_data,
|
||||
}
|
||||
|
||||
now = dt_util.utcnow()
|
||||
last_reset = dt_util.utc_from_timestamp(0).isoformat()
|
||||
last_reset_cost_sensor = now.isoformat()
|
||||
|
||||
# Optionally initialize dependent entities
|
||||
if initial_energy is not None:
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
initial_energy,
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
hass.states.async_set("sensor.energy_price", "1")
|
||||
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
await setup_integration(hass)
|
||||
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == initial_cost
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||
if initial_cost != "unknown":
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||
|
||||
# Optional late setup of dependent entities
|
||||
if initial_energy is None:
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"0",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "0.0"
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
assert state.attributes[ATTR_STATE_CLASS] == "measurement"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR"
|
||||
|
||||
# # Unique ID temp disabled
|
||||
# # entity_registry = er.async_get(hass)
|
||||
# # entry = entity_registry.async_get(cost_sensor_entity_id)
|
||||
# # assert entry.unique_id == "energy_energy_consumption cost"
|
||||
|
||||
# Energy use bumped to 10 kWh
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"10",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "10.0" # 0 EUR + (10-0) kWh * 1 EUR/kWh = 10 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Nothing happens when price changes
|
||||
if price_entity is not None:
|
||||
hass.states.async_set(price_entity, "2")
|
||||
await hass.async_block_till_done()
|
||||
else:
|
||||
energy_data = copy.deepcopy(energy_data)
|
||||
energy_data["energy_sources"][0][flow_type][0]["number_energy_price"] = 2
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({"id": 5, "type": "energy/save_prefs", **energy_data})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "10.0" # 10 EUR + (10-10) kWh * 2 EUR/kWh = 10 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Additional consumption is using the new price
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"14.5",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "19.0" # 10 EUR + (14.5-10) kWh * 2 EUR/kWh = 19 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Check generated statistics
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
||||
assert cost_sensor_entity_id in statistics
|
||||
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0
|
||||
|
||||
# Energy sensor has a small dip
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"14",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
||||
last_reset = (now + timedelta(seconds=1)).isoformat()
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"4",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR
|
||||
assert state.attributes["last_reset"] != last_reset_cost_sensor
|
||||
last_reset_cost_sensor = state.attributes["last_reset"]
|
||||
|
||||
# Energy use bumped to 10 kWh
|
||||
hass.states.async_set(
|
||||
usage_sensor_entity_id,
|
||||
"10",
|
||||
{**energy_attributes, **{"last_reset": last_reset}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(cost_sensor_entity_id)
|
||||
assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR
|
||||
assert state.attributes["last_reset"] == last_reset_cost_sensor
|
||||
|
||||
# Check generated statistics
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
@@ -284,6 +501,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
# Initial state: 10kWh
|
||||
hass.states.async_set(
|
||||
"sensor.energy_consumption",
|
||||
10000,
|
||||
@@ -296,7 +514,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
||||
state = hass.states.get("sensor.energy_consumption_cost")
|
||||
assert state.state == "0.0"
|
||||
|
||||
# Energy use bumped to 10 kWh
|
||||
# Energy use bumped by 10 kWh
|
||||
hass.states.async_set(
|
||||
"sensor.energy_consumption",
|
||||
20000,
|
||||
@@ -361,7 +579,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None:
|
||||
async def test_cost_sensor_wrong_state_class(
|
||||
hass, hass_storage, caplog, state_class
|
||||
) -> None:
|
||||
"""Test energy sensor rejects wrong state_class."""
|
||||
"""Test energy sensor rejects state_class with wrong state_class."""
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||
ATTR_STATE_CLASS: state_class,
|
||||
@@ -417,3 +635,61 @@ async def test_cost_sensor_wrong_state_class(
|
||||
|
||||
state = hass.states.get("sensor.energy_consumption_cost")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||
async def test_cost_sensor_state_class_measurement_no_reset(
|
||||
hass, hass_storage, caplog, state_class
|
||||
) -> None:
|
||||
"""Test energy sensor rejects state_class with no last_reset."""
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
|
||||
ATTR_STATE_CLASS: state_class,
|
||||
}
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
energy_data["energy_sources"].append(
|
||||
{
|
||||
"type": "grid",
|
||||
"flow_from": [
|
||||
{
|
||||
"stat_energy_from": "sensor.energy_consumption",
|
||||
"entity_energy_from": "sensor.energy_consumption",
|
||||
"stat_cost": None,
|
||||
"entity_energy_price": None,
|
||||
"number_energy_price": 0.5,
|
||||
}
|
||||
],
|
||||
"flow_to": [],
|
||||
"cost_adjustment_day": 0,
|
||||
}
|
||||
)
|
||||
|
||||
hass_storage[data.STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": energy_data,
|
||||
}
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
hass.states.async_set(
|
||||
"sensor.energy_consumption",
|
||||
10000,
|
||||
energy_attributes,
|
||||
)
|
||||
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
await setup_integration(hass)
|
||||
|
||||
state = hass.states.get("sensor.energy_consumption_cost")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Energy use bumped to 10 kWh
|
||||
hass.states.async_set(
|
||||
"sensor.energy_consumption",
|
||||
20000,
|
||||
energy_attributes,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_consumption_cost")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
@@ -382,15 +382,6 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
||||
"value": "123,123.12",
|
||||
},
|
||||
),
|
||||
(
|
||||
"-100",
|
||||
"$/kWh",
|
||||
{
|
||||
"type": "entity_negative_state",
|
||||
"identifier": "sensor.grid_price_1",
|
||||
"value": -100.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
"123",
|
||||
"$/Ws",
|
||||
@@ -414,7 +405,7 @@ async def test_validation_grid_price_errors(
|
||||
hass.states.async_set(
|
||||
"sensor.grid_price_1",
|
||||
state,
|
||||
{"unit_of_measurement": unit, "state_class": "total_increasing"},
|
||||
{"unit_of_measurement": unit, "state_class": "measurement"},
|
||||
)
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
@@ -441,3 +432,59 @@ async def test_validation_grid_price_errors(
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded):
|
||||
"""Test validating gas with sensors for energy and cost/compensation."""
|
||||
mock_is_entity_recorded["sensor.gas_cost_1"] = False
|
||||
mock_is_entity_recorded["sensor.gas_compensation_1"] = False
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "gas",
|
||||
"stat_energy_from": "sensor.gas_consumption_1",
|
||||
"stat_cost": "sensor.gas_cost_1",
|
||||
},
|
||||
{
|
||||
"type": "gas",
|
||||
"stat_energy_from": "sensor.gas_consumption_2",
|
||||
"stat_cost": "sensor.gas_cost_2",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.gas_consumption_1",
|
||||
"10.10",
|
||||
{"unit_of_measurement": "beers", "state_class": "total_increasing"},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.gas_consumption_2",
|
||||
"10.10",
|
||||
{"unit_of_measurement": "kWh", "state_class": "total_increasing"},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.gas_cost_2",
|
||||
"10.10",
|
||||
{"unit_of_measurement": "EUR/kWh", "state_class": "total_increasing"},
|
||||
)
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [
|
||||
[
|
||||
{
|
||||
"type": "entity_unexpected_unit_gas",
|
||||
"identifier": "sensor.gas_consumption_1",
|
||||
"value": "beers",
|
||||
},
|
||||
{
|
||||
"type": "recorder_untracked",
|
||||
"identifier": "sensor.gas_cost_1",
|
||||
"value": None,
|
||||
},
|
||||
],
|
||||
[],
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Test honeywell setup process."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)
|
||||
async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry):
|
||||
"""Initialize the config entry."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@@ -15,6 +18,7 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry):
|
||||
assert hass.states.async_entity_ids_count() == 1
|
||||
|
||||
|
||||
@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)
|
||||
async def test_setup_multiple_thermostats(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, location, another_device
|
||||
) -> None:
|
||||
|
||||
@@ -81,7 +81,6 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"platform": "integration",
|
||||
"name": "integration",
|
||||
"source": "sensor.power",
|
||||
"unit": ENERGY_KILO_WATT_HOUR,
|
||||
"round": 2,
|
||||
}
|
||||
}
|
||||
@@ -114,7 +113,6 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None:
|
||||
"platform": "integration",
|
||||
"name": "integration",
|
||||
"source": "sensor.power",
|
||||
"unit": ENERGY_KILO_WATT_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +121,10 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get("sensor.integration")
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR
|
||||
assert state.state == "unavailable"
|
||||
assert state.attributes.get("unit_of_measurement") is None
|
||||
assert state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING
|
||||
|
||||
assert "device_class" not in state.attributes
|
||||
|
||||
|
||||
|
||||
@@ -1289,6 +1289,45 @@ async def test_logbook_entity_matches_only(hass, hass_client):
|
||||
assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
|
||||
|
||||
|
||||
async def test_custom_log_entry_discoverable_via_entity_matches_only(hass, hass_client):
|
||||
"""Test if a custom log entry is later discoverable via entity_matches_only."""
|
||||
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||
await async_setup_component(hass, "logbook", {})
|
||||
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||
|
||||
logbook.async_log_entry(
|
||||
hass,
|
||||
"Alarm",
|
||||
"is triggered",
|
||||
"switch",
|
||||
"switch.test_switch",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_add_executor_job(trigger_db_commit, hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
# Today time 00:00:00
|
||||
start = dt_util.utcnow().date()
|
||||
start_date = datetime(start.year, start.month, start.day)
|
||||
|
||||
# Test today entries with filter by end_time
|
||||
end_time = start + timedelta(hours=24)
|
||||
response = await client.get(
|
||||
f"/api/logbook/{start_date.isoformat()}?end_time={end_time.isoformat()}&entity=switch.test_switch&entity_matches_only"
|
||||
)
|
||||
assert response.status == 200
|
||||
json_dict = await response.json()
|
||||
|
||||
assert len(json_dict) == 1
|
||||
|
||||
assert json_dict[0]["name"] == "Alarm"
|
||||
assert json_dict[0]["message"] == "is triggered"
|
||||
assert json_dict[0]["entity_id"] == "switch.test_switch"
|
||||
|
||||
|
||||
async def test_logbook_entity_matches_only_multiple(hass, hass_client):
|
||||
"""Test the logbook view with a multiple entities and entity_matches_only."""
|
||||
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch, sentinel
|
||||
|
||||
import pytest
|
||||
from pytest import approx
|
||||
|
||||
from homeassistant.components.recorder import history
|
||||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
||||
from homeassistant.components.recorder.models import (
|
||||
Statistics,
|
||||
process_timestamp_to_utc_isoformat,
|
||||
)
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
get_last_statistics,
|
||||
statistics_during_period,
|
||||
@@ -32,7 +36,7 @@ def test_compile_hourly_statistics(hass_recorder):
|
||||
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
||||
stats = statistics_during_period(hass, zero, **kwargs)
|
||||
assert stats == {}
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1", True)
|
||||
assert stats == {}
|
||||
|
||||
recorder.do_adhoc_statistics(period="hourly", start=zero)
|
||||
@@ -78,22 +82,121 @@ def test_compile_hourly_statistics(hass_recorder):
|
||||
assert stats == {}
|
||||
|
||||
# Test get_last_statistics
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1", True)
|
||||
assert stats == {}
|
||||
|
||||
stats = get_last_statistics(hass, 1, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 1, "sensor.test1", True)
|
||||
assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]}
|
||||
|
||||
stats = get_last_statistics(hass, 2, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 2, "sensor.test1", True)
|
||||
assert stats == {"sensor.test1": expected_stats1[::-1]}
|
||||
|
||||
stats = get_last_statistics(hass, 3, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 3, "sensor.test1", True)
|
||||
assert stats == {"sensor.test1": expected_stats1[::-1]}
|
||||
|
||||
stats = get_last_statistics(hass, 1, "sensor.test3")
|
||||
stats = get_last_statistics(hass, 1, "sensor.test3", True)
|
||||
assert stats == {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sensor_statistics():
|
||||
"""Generate some fake statistics."""
|
||||
sensor_stats = {
|
||||
"meta": {"unit_of_measurement": "dogs", "has_mean": True, "has_sum": False},
|
||||
"stat": {},
|
||||
}
|
||||
|
||||
def get_fake_stats():
|
||||
return {
|
||||
"sensor.test1": sensor_stats,
|
||||
"sensor.test2": sensor_stats,
|
||||
"sensor.test3": sensor_stats,
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sensor.recorder.compile_statistics",
|
||||
return_value=get_fake_stats(),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_from_stats():
|
||||
"""Mock out Statistics.from_stats."""
|
||||
counter = 0
|
||||
real_from_stats = Statistics.from_stats
|
||||
|
||||
def from_stats(metadata_id, start, stats):
|
||||
nonlocal counter
|
||||
if counter == 0 and metadata_id == 2:
|
||||
counter += 1
|
||||
return None
|
||||
return real_from_stats(metadata_id, start, stats)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.statistics.Statistics.from_stats",
|
||||
side_effect=from_stats,
|
||||
autospec=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def test_compile_hourly_statistics_exception(
|
||||
hass_recorder, mock_sensor_statistics, mock_from_stats
|
||||
):
|
||||
"""Test exception handling when compiling hourly statistics."""
|
||||
|
||||
def mock_from_stats():
|
||||
raise ValueError
|
||||
|
||||
hass = hass_recorder()
|
||||
recorder = hass.data[DATA_INSTANCE]
|
||||
setup_component(hass, "sensor", {})
|
||||
|
||||
now = dt_util.utcnow()
|
||||
recorder.do_adhoc_statistics(period="hourly", start=now)
|
||||
recorder.do_adhoc_statistics(period="hourly", start=now + timedelta(hours=1))
|
||||
wait_recording_done(hass)
|
||||
expected_1 = {
|
||||
"statistic_id": "sensor.test1",
|
||||
"start": process_timestamp_to_utc_isoformat(now),
|
||||
"mean": None,
|
||||
"min": None,
|
||||
"max": None,
|
||||
"last_reset": None,
|
||||
"state": None,
|
||||
"sum": None,
|
||||
}
|
||||
expected_2 = {
|
||||
"statistic_id": "sensor.test1",
|
||||
"start": process_timestamp_to_utc_isoformat(now + timedelta(hours=1)),
|
||||
"mean": None,
|
||||
"min": None,
|
||||
"max": None,
|
||||
"last_reset": None,
|
||||
"state": None,
|
||||
"sum": None,
|
||||
}
|
||||
expected_stats1 = [
|
||||
{**expected_1, "statistic_id": "sensor.test1"},
|
||||
{**expected_2, "statistic_id": "sensor.test1"},
|
||||
]
|
||||
expected_stats2 = [
|
||||
{**expected_2, "statistic_id": "sensor.test2"},
|
||||
]
|
||||
expected_stats3 = [
|
||||
{**expected_1, "statistic_id": "sensor.test3"},
|
||||
{**expected_2, "statistic_id": "sensor.test3"},
|
||||
]
|
||||
|
||||
stats = statistics_during_period(hass, now)
|
||||
assert stats == {
|
||||
"sensor.test1": expected_stats1,
|
||||
"sensor.test2": expected_stats2,
|
||||
"sensor.test3": expected_stats3,
|
||||
}
|
||||
|
||||
|
||||
def test_rename_entity(hass_recorder):
|
||||
"""Test statistics is migrated when entity_id is changed."""
|
||||
hass = hass_recorder()
|
||||
@@ -116,7 +219,7 @@ def test_rename_entity(hass_recorder):
|
||||
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
||||
stats = statistics_during_period(hass, zero, **kwargs)
|
||||
assert stats == {}
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1")
|
||||
stats = get_last_statistics(hass, 0, "sensor.test1", True)
|
||||
assert stats == {}
|
||||
|
||||
recorder.do_adhoc_statistics(period="hourly", start=zero)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for sensor recorder platform."""
|
||||
# pylint: disable=protected-access,invalid-name
|
||||
from datetime import timedelta
|
||||
import math
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -17,6 +18,7 @@ from homeassistant.components.recorder.statistics import (
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.setup import setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
|
||||
|
||||
from tests.components.recorder.common import wait_recording_done
|
||||
|
||||
@@ -50,18 +52,18 @@ GAS_SENSOR_ATTRIBUTES = {
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,mean,min,max",
|
||||
[
|
||||
(None, "%", "%", 16.440677, 10, 30),
|
||||
("battery", "%", "%", 16.440677, 10, 30),
|
||||
("battery", None, None, 16.440677, 10, 30),
|
||||
("humidity", "%", "%", 16.440677, 10, 30),
|
||||
("humidity", None, None, 16.440677, 10, 30),
|
||||
("pressure", "Pa", "Pa", 16.440677, 10, 30),
|
||||
("pressure", "hPa", "Pa", 1644.0677, 1000, 3000),
|
||||
("pressure", "mbar", "Pa", 1644.0677, 1000, 3000),
|
||||
("pressure", "inHg", "Pa", 55674.53, 33863.89, 101591.67),
|
||||
("pressure", "psi", "Pa", 113354.48, 68947.57, 206842.71),
|
||||
("temperature", "°C", "°C", 16.440677, 10, 30),
|
||||
("temperature", "°F", "°C", -8.644068, -12.22222, -1.111111),
|
||||
(None, "%", "%", 13.050847, -10, 30),
|
||||
("battery", "%", "%", 13.050847, -10, 30),
|
||||
("battery", None, None, 13.050847, -10, 30),
|
||||
("humidity", "%", "%", 13.050847, -10, 30),
|
||||
("humidity", None, None, 13.050847, -10, 30),
|
||||
("pressure", "Pa", "Pa", 13.050847, -10, 30),
|
||||
("pressure", "hPa", "Pa", 1305.0847, -1000, 3000),
|
||||
("pressure", "mbar", "Pa", 1305.0847, -1000, 3000),
|
||||
("pressure", "inHg", "Pa", 44195.25, -33863.89, 101591.67),
|
||||
("pressure", "psi", "Pa", 89982.42, -68947.57, 206842.71),
|
||||
("temperature", "°C", "°C", 13.050847, -10, 30),
|
||||
("temperature", "°F", "°C", -10.52731, -23.33333, -1.111111),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics(
|
||||
@@ -155,8 +157,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
||||
{
|
||||
"statistic_id": "sensor.test1",
|
||||
"start": process_timestamp_to_utc_isoformat(zero),
|
||||
"mean": approx(16.440677966101696),
|
||||
"min": approx(10.0),
|
||||
"mean": approx(13.050847),
|
||||
"min": approx(-10.0),
|
||||
"max": approx(30.0),
|
||||
"last_reset": None,
|
||||
"state": None,
|
||||
@@ -167,8 +169,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
||||
{
|
||||
"statistic_id": "sensor.test6",
|
||||
"start": process_timestamp_to_utc_isoformat(zero),
|
||||
"mean": approx(16.440677966101696),
|
||||
"min": approx(10.0),
|
||||
"mean": approx(13.050847),
|
||||
"min": approx(-10.0),
|
||||
"max": approx(30.0),
|
||||
"last_reset": None,
|
||||
"state": None,
|
||||
@@ -179,8 +181,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
||||
{
|
||||
"statistic_id": "sensor.test7",
|
||||
"start": process_timestamp_to_utc_isoformat(zero),
|
||||
"mean": approx(16.440677966101696),
|
||||
"min": approx(10.0),
|
||||
"mean": approx(13.050847),
|
||||
"min": approx(-10.0),
|
||||
"max": approx(30.0),
|
||||
"last_reset": None,
|
||||
"state": None,
|
||||
@@ -193,22 +195,29 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
|
||||
|
||||
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,factor",
|
||||
"units,device_class,unit,display_unit,factor",
|
||||
[
|
||||
("energy", "kWh", "kWh", 1),
|
||||
("energy", "Wh", "kWh", 1 / 1000),
|
||||
("monetary", "EUR", "EUR", 1),
|
||||
("monetary", "SEK", "SEK", 1),
|
||||
("gas", "m³", "m³", 1),
|
||||
("gas", "ft³", "m³", 0.0283168466),
|
||||
(IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1),
|
||||
(IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000),
|
||||
(IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1),
|
||||
(IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1),
|
||||
(IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711),
|
||||
(IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1),
|
||||
(METRIC_SYSTEM, "energy", "kWh", "kWh", 1),
|
||||
(METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000),
|
||||
(METRIC_SYSTEM, "monetary", "EUR", "EUR", 1),
|
||||
(METRIC_SYSTEM, "monetary", "SEK", "SEK", 1),
|
||||
(METRIC_SYSTEM, "gas", "m³", "m³", 1),
|
||||
(METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_sum_statistics_amount(
|
||||
hass_recorder, caplog, state_class, device_class, unit, native_unit, factor
|
||||
hass_recorder, caplog, units, state_class, device_class, unit, display_unit, factor
|
||||
):
|
||||
"""Test compiling hourly statistics."""
|
||||
zero = dt_util.utcnow()
|
||||
hass = hass_recorder()
|
||||
hass.config.units = units
|
||||
recorder = hass.data[DATA_INSTANCE]
|
||||
setup_component(hass, "sensor", {})
|
||||
attributes = {
|
||||
@@ -235,7 +244,7 @@ def test_compile_hourly_sum_statistics_amount(
|
||||
wait_recording_done(hass)
|
||||
statistic_ids = list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
|
||||
{"statistic_id": "sensor.test1", "unit_of_measurement": display_unit}
|
||||
]
|
||||
stats = statistics_during_period(hass, zero)
|
||||
assert stats == {
|
||||
@@ -349,6 +358,70 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
|
||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,factor",
|
||||
[
|
||||
("energy", "kWh", "kWh", 1),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_sum_statistics_nan_inf_state(
|
||||
hass_recorder, caplog, state_class, device_class, unit, native_unit, factor
|
||||
):
|
||||
"""Test compiling hourly statistics with nan and inf states."""
|
||||
zero = dt_util.utcnow()
|
||||
hass = hass_recorder()
|
||||
recorder = hass.data[DATA_INSTANCE]
|
||||
setup_component(hass, "sensor", {})
|
||||
attributes = {
|
||||
"device_class": device_class,
|
||||
"state_class": state_class,
|
||||
"unit_of_measurement": unit,
|
||||
"last_reset": None,
|
||||
}
|
||||
seq = [10, math.nan, 15, 15, 20, math.inf, 20, 10]
|
||||
|
||||
states = {"sensor.test1": []}
|
||||
one = zero
|
||||
for i in range(len(seq)):
|
||||
one = one + timedelta(minutes=1)
|
||||
_states = record_meter_state(
|
||||
hass, one, "sensor.test1", attributes, seq[i : i + 1]
|
||||
)
|
||||
states["sensor.test1"].extend(_states["sensor.test1"])
|
||||
|
||||
hist = history.get_significant_states(
|
||||
hass,
|
||||
zero - timedelta.resolution,
|
||||
one + timedelta.resolution,
|
||||
significant_changes_only=False,
|
||||
)
|
||||
assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"]
|
||||
|
||||
recorder.do_adhoc_statistics(period="hourly", start=zero)
|
||||
wait_recording_done(hass)
|
||||
statistic_ids = list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
|
||||
]
|
||||
stats = statistics_during_period(hass, zero)
|
||||
assert stats == {
|
||||
"sensor.test1": [
|
||||
{
|
||||
"statistic_id": "sensor.test1",
|
||||
"start": process_timestamp_to_utc_isoformat(zero),
|
||||
"max": None,
|
||||
"mean": None,
|
||||
"min": None,
|
||||
"last_reset": process_timestamp_to_utc_isoformat(one),
|
||||
"state": approx(factor * seq[7]),
|
||||
"sum": approx(factor * (seq[2] + seq[3] + seq[4] + seq[6] + seq[7])),
|
||||
},
|
||||
]
|
||||
}
|
||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,factor",
|
||||
[
|
||||
@@ -988,10 +1061,10 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes):
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,mean,min,max",
|
||||
[
|
||||
(None, None, None, 16.440677, 10, 30),
|
||||
(None, "%", "%", 16.440677, 10, 30),
|
||||
("battery", "%", "%", 16.440677, 10, 30),
|
||||
("battery", None, None, 16.440677, 10, 30),
|
||||
(None, None, None, 13.050847, -10, 30),
|
||||
(None, "%", "%", 13.050847, -10, 30),
|
||||
("battery", "%", "%", 13.050847, -10, 30),
|
||||
("battery", None, None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_units_1(
|
||||
@@ -1074,10 +1147,10 @@ def test_compile_hourly_statistics_changing_units_1(
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,mean,min,max",
|
||||
[
|
||||
(None, None, None, 16.440677, 10, 30),
|
||||
(None, "%", "%", 16.440677, 10, 30),
|
||||
("battery", "%", "%", 16.440677, 10, 30),
|
||||
("battery", None, None, 16.440677, 10, 30),
|
||||
(None, None, None, 13.050847, -10, 30),
|
||||
(None, "%", "%", 13.050847, -10, 30),
|
||||
("battery", "%", "%", 13.050847, -10, 30),
|
||||
("battery", None, None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_units_2(
|
||||
@@ -1119,10 +1192,10 @@ def test_compile_hourly_statistics_changing_units_2(
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,mean,min,max",
|
||||
[
|
||||
(None, None, None, 16.440677, 10, 30),
|
||||
(None, "%", "%", 16.440677, 10, 30),
|
||||
("battery", "%", "%", 16.440677, 10, 30),
|
||||
("battery", None, None, 16.440677, 10, 30),
|
||||
(None, None, None, 13.050847, -10, 30),
|
||||
(None, "%", "%", 13.050847, -10, 30),
|
||||
("battery", "%", "%", 13.050847, -10, 30),
|
||||
("battery", None, None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_units_3(
|
||||
@@ -1203,7 +1276,7 @@ def test_compile_hourly_statistics_changing_units_3(
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,unit,native_unit,mean,min,max",
|
||||
[
|
||||
(None, None, None, 16.440677, 10, 30),
|
||||
(None, None, None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_statistics(
|
||||
@@ -1309,7 +1382,7 @@ def record_states(hass, zero, entity_id, attributes):
|
||||
|
||||
states = {entity_id: []}
|
||||
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
|
||||
states[entity_id].append(set_state(entity_id, "10", attributes=attributes))
|
||||
states[entity_id].append(set_state(entity_id, "-10", attributes=attributes))
|
||||
|
||||
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
|
||||
states[entity_id].append(set_state(entity_id, "15", attributes=attributes))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Test the sensor significant change platform."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor.significant_change import (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -12,48 +15,54 @@ from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
|
||||
AQI_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_AQI,
|
||||
}
|
||||
|
||||
async def test_significant_change_temperature():
|
||||
BATTERY_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
|
||||
}
|
||||
|
||||
HUMIDITY_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
}
|
||||
|
||||
TEMP_CELSIUS_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
}
|
||||
|
||||
TEMP_FREEDOM_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"old_state,new_state,attrs,result",
|
||||
[
|
||||
("0", "1", AQI_ATTRS, True),
|
||||
("1", "0", AQI_ATTRS, True),
|
||||
("0.1", "0.5", AQI_ATTRS, False),
|
||||
("0.5", "0.1", AQI_ATTRS, False),
|
||||
("99", "100", AQI_ATTRS, False),
|
||||
("100", "99", AQI_ATTRS, False),
|
||||
("101", "99", AQI_ATTRS, False),
|
||||
("99", "101", AQI_ATTRS, True),
|
||||
("100", "100", BATTERY_ATTRS, False),
|
||||
("100", "99", BATTERY_ATTRS, True),
|
||||
("100", "100", HUMIDITY_ATTRS, False),
|
||||
("100", "99", HUMIDITY_ATTRS, True),
|
||||
("12", "12", TEMP_CELSIUS_ATTRS, False),
|
||||
("12", "13", TEMP_CELSIUS_ATTRS, True),
|
||||
("12.1", "12.2", TEMP_CELSIUS_ATTRS, False),
|
||||
("70", "71", TEMP_FREEDOM_ATTRS, True),
|
||||
("70", "70.5", TEMP_FREEDOM_ATTRS, False),
|
||||
],
|
||||
)
|
||||
async def test_significant_change_temperature(old_state, new_state, attrs, result):
|
||||
"""Detect temperature significant changes."""
|
||||
celsius_attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
}
|
||||
assert not async_check_significant_change(
|
||||
None, "12", celsius_attrs, "12", celsius_attrs
|
||||
assert (
|
||||
async_check_significant_change(None, old_state, attrs, new_state, attrs)
|
||||
is result
|
||||
)
|
||||
assert async_check_significant_change(
|
||||
None, "12", celsius_attrs, "13", celsius_attrs
|
||||
)
|
||||
assert not async_check_significant_change(
|
||||
None, "12.1", celsius_attrs, "12.2", celsius_attrs
|
||||
)
|
||||
|
||||
freedom_attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
|
||||
}
|
||||
assert async_check_significant_change(
|
||||
None, "70", freedom_attrs, "71", freedom_attrs
|
||||
)
|
||||
assert not async_check_significant_change(
|
||||
None, "70", freedom_attrs, "70.5", freedom_attrs
|
||||
)
|
||||
|
||||
|
||||
async def test_significant_change_battery():
|
||||
"""Detect battery significant changes."""
|
||||
attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
|
||||
}
|
||||
assert not async_check_significant_change(None, "100", attrs, "100", attrs)
|
||||
assert async_check_significant_change(None, "100", attrs, "99", attrs)
|
||||
|
||||
|
||||
async def test_significant_change_humidity():
|
||||
"""Detect humidity significant changes."""
|
||||
attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
}
|
||||
assert not async_check_significant_change(None, "100", attrs, "100", attrs)
|
||||
assert async_check_significant_change(None, "100", attrs, "99", attrs)
|
||||
|
||||
@@ -991,9 +991,6 @@ async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_
|
||||
|
||||
@callback
|
||||
def _callback(*_):
|
||||
import pprint
|
||||
|
||||
pprint.pprint(mock_ssdp_response)
|
||||
hass.async_create_task(listener.async_callback(mock_ssdp_response))
|
||||
|
||||
listener.async_start = _async_callback
|
||||
@@ -1050,3 +1047,113 @@ async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_
|
||||
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
|
||||
== mock_good_ip_ssdp_response["location"]
|
||||
)
|
||||
|
||||
|
||||
async def test_location_change_with_overlapping_udn_st_combinations(
|
||||
hass, aioclient_mock
|
||||
):
|
||||
"""Test handling when a UDN and ST broadcast multiple locations."""
|
||||
mock_get_ssdp = {
|
||||
"test_integration": [
|
||||
{"manufacturer": "test_manufacturer", "modelName": "test_model"}
|
||||
]
|
||||
}
|
||||
|
||||
hue_response = """
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<device>
|
||||
<manufacturer>test_manufacturer</manufacturer>
|
||||
<modelName>test_model</modelName>
|
||||
</device>
|
||||
</root>
|
||||
"""
|
||||
|
||||
aioclient_mock.get(
|
||||
"http://192.168.72.1:49154/wps_device.xml",
|
||||
text=hue_response.format(ip_address="192.168.72.1"),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://192.168.72.1:49152/wps_device.xml",
|
||||
text=hue_response.format(ip_address="192.168.72.1"),
|
||||
)
|
||||
ssdp_response_without_location = {
|
||||
"ST": "upnp:rootdevice",
|
||||
"_udn": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6",
|
||||
"USN": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6::upnp:rootdevice",
|
||||
"EXT": "",
|
||||
}
|
||||
|
||||
port_49154_response = CaseInsensitiveDict(
|
||||
**ssdp_response_without_location,
|
||||
**{"LOCATION": "http://192.168.72.1:49154/wps_device.xml"},
|
||||
)
|
||||
port_49152_response = CaseInsensitiveDict(
|
||||
**ssdp_response_without_location,
|
||||
**{"LOCATION": "http://192.168.72.1:49152/wps_device.xml"},
|
||||
)
|
||||
mock_ssdp_response = port_49154_response
|
||||
|
||||
def _generate_fake_ssdp_listener(*args, **kwargs):
|
||||
listener = SSDPListener(*args, **kwargs)
|
||||
|
||||
async def _async_callback(*_):
|
||||
pass
|
||||
|
||||
@callback
|
||||
def _callback(*_):
|
||||
hass.async_create_task(listener.async_callback(mock_ssdp_response))
|
||||
|
||||
listener.async_start = _async_callback
|
||||
listener.async_search = _callback
|
||||
return listener
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ssdp.async_get_ssdp",
|
||||
return_value=mock_get_ssdp,
|
||||
), patch(
|
||||
"homeassistant.components.ssdp.SSDPListener",
|
||||
new=_generate_fake_ssdp_listener,
|
||||
), patch.object(
|
||||
hass.config_entries.flow, "async_init"
|
||||
) as mock_init:
|
||||
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_init.mock_calls) == 1
|
||||
assert mock_init.mock_calls[0][1][0] == "test_integration"
|
||||
assert mock_init.mock_calls[0][2]["context"] == {
|
||||
"source": config_entries.SOURCE_SSDP
|
||||
}
|
||||
assert (
|
||||
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
|
||||
== port_49154_response["location"]
|
||||
)
|
||||
|
||||
mock_init.reset_mock()
|
||||
mock_ssdp_response = port_49152_response
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=400))
|
||||
await hass.async_block_till_done()
|
||||
assert mock_init.mock_calls[0][1][0] == "test_integration"
|
||||
assert mock_init.mock_calls[0][2]["context"] == {
|
||||
"source": config_entries.SOURCE_SSDP
|
||||
}
|
||||
assert (
|
||||
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
|
||||
== port_49152_response["location"]
|
||||
)
|
||||
|
||||
mock_init.reset_mock()
|
||||
mock_ssdp_response = port_49154_response
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=600))
|
||||
await hass.async_block_till_done()
|
||||
assert mock_init.mock_calls[0][1][0] == "test_integration"
|
||||
assert mock_init.mock_calls[0][2]["context"] == {
|
||||
"source": config_entries.SOURCE_SSDP
|
||||
}
|
||||
assert (
|
||||
mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
|
||||
== port_49154_response["location"]
|
||||
)
|
||||
|
||||
@@ -7,12 +7,16 @@ from surepy import MESTART_RESOURCE
|
||||
from . import MOCK_API_DATA
|
||||
|
||||
|
||||
async def _mock_call(method, resource):
|
||||
if method == "GET" and resource == MESTART_RESOURCE:
|
||||
return {"data": MOCK_API_DATA}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def surepetcare():
|
||||
"""Mock the SurePetcare for easier testing."""
|
||||
with patch("surepy.SureAPIClient", autospec=True) as mock_client_class, patch(
|
||||
"surepy.find_token"
|
||||
):
|
||||
with patch("surepy.SureAPIClient", autospec=True) as mock_client_class:
|
||||
client = mock_client_class.return_value
|
||||
client.resources = {MESTART_RESOURCE: {"data": MOCK_API_DATA}}
|
||||
client.resources = {}
|
||||
client.call = _mock_call
|
||||
yield client
|
||||
|
||||
@@ -12,7 +12,7 @@ EXPECTED_ENTITY_IDS = {
|
||||
}
|
||||
|
||||
|
||||
async def test_binary_sensors(hass, surepetcare) -> None:
|
||||
async def test_sensors(hass, surepetcare) -> None:
|
||||
"""Test the generation of unique ids."""
|
||||
assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -60,6 +60,38 @@ async def test_missing_optional_config(hass, calls):
|
||||
_verify(hass, "a", ["a", "b"])
|
||||
|
||||
|
||||
async def test_multiple_configs(hass, calls):
|
||||
"""Test: multiple select entities get created."""
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"select": [
|
||||
{
|
||||
"state": "{{ 'a' }}",
|
||||
"select_option": {"service": "script.select_option"},
|
||||
"options": "{{ ['a', 'b'] }}",
|
||||
},
|
||||
{
|
||||
"state": "{{ 'a' }}",
|
||||
"select_option": {"service": "script.select_option"},
|
||||
"options": "{{ ['a', 'b'] }}",
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_verify(hass, "a", ["a", "b"])
|
||||
_verify(hass, "a", ["a", "b"], f"{_TEST_SELECT}_2")
|
||||
|
||||
|
||||
async def test_missing_required_keys(hass, calls):
|
||||
"""Test: missing required fields will fail."""
|
||||
with assert_setup_component(0, "template"):
|
||||
@@ -250,9 +282,9 @@ async def test_trigger_select(hass):
|
||||
assert events[0].event_type == "test_number_event"
|
||||
|
||||
|
||||
def _verify(hass, expected_current_option, expected_options):
|
||||
def _verify(hass, expected_current_option, expected_options, entity_name=_TEST_SELECT):
|
||||
"""Verify select's state."""
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
state = hass.states.get(entity_name)
|
||||
attributes = state.attributes
|
||||
assert state.state == str(expected_current_option)
|
||||
assert attributes.get(SELECT_ATTR_OPTIONS) == expected_options
|
||||
|
||||
@@ -1038,6 +1038,7 @@ async def test_trigger_entity(hass):
|
||||
"unique_id": "via_list-id",
|
||||
"device_class": "battery",
|
||||
"unit_of_measurement": "%",
|
||||
"availability": "{{ True }}",
|
||||
"state": "{{ trigger.event.data.beer + 1 }}",
|
||||
"picture": "{{ '/local/dogs.png' }}",
|
||||
"icon": "{{ 'mdi:pirate' }}",
|
||||
@@ -1197,3 +1198,44 @@ async def test_config_top_level(hass):
|
||||
assert state.state == "5"
|
||||
assert state.attributes["device_class"] == "battery"
|
||||
assert state.attributes["state_class"] == "measurement"
|
||||
|
||||
|
||||
async def test_trigger_entity_available(hass):
|
||||
"""Test trigger entity availability works."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"sensor": [
|
||||
{
|
||||
"name": "Maybe Available",
|
||||
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
|
||||
"state": "{{ trigger.event.data.beer }}",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Sensors are unknown if never triggered
|
||||
state = hass.states.get("sensor.maybe_available")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
hass.bus.async_fire("test_event", {"beer": 2})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.maybe_available")
|
||||
assert state.state == "2"
|
||||
|
||||
hass.bus.async_fire("test_event", {"beer": 1})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.maybe_available")
|
||||
assert state.state == "unavailable"
|
||||
|
||||
@@ -52,55 +52,6 @@ def mock_venv():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
)
|
||||
async def test_discovered_by_observer_before_started(hass, operating_system):
|
||||
"""Test a device is discovered by the observer before started."""
|
||||
|
||||
async def _mock_monitor_observer_callback(callback):
|
||||
await hass.async_add_executor_job(
|
||||
callback, MagicMock(action="add", device_path="/dev/new")
|
||||
)
|
||||
|
||||
def _create_mock_monitor_observer(monitor, callback, name):
|
||||
hass.async_create_task(_mock_monitor_observer_callback(callback))
|
||||
return MagicMock()
|
||||
|
||||
new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}]
|
||||
|
||||
mock_comports = [
|
||||
MagicMock(
|
||||
device=slae_sh_device.device,
|
||||
vid=12345,
|
||||
pid=12345,
|
||||
serial_number=slae_sh_device.serial_number,
|
||||
manufacturer=slae_sh_device.manufacturer,
|
||||
description=slae_sh_device.description,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||
), patch(
|
||||
"homeassistant.components.usb.comports", return_value=mock_comports
|
||||
), patch(
|
||||
"pyudev.MonitorObserver", new=_create_mock_monitor_observer
|
||||
):
|
||||
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch("homeassistant.components.usb.comports", return_value=[]), patch.object(
|
||||
hass.config_entries.flow, "async_init"
|
||||
) as mock_config_flow:
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_config_flow.mock_calls) == 1
|
||||
assert mock_config_flow.mock_calls[0][1][0] == "test1"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
|
||||
@@ -113,6 +113,34 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass):
|
||||
}
|
||||
|
||||
|
||||
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
|
||||
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
|
||||
async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass):
|
||||
"""Test zeroconf flow that was ignored gets updated."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="tube_zb_gw_cc2652p2_poe",
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
service_info = {
|
||||
"host": "192.168.1.22",
|
||||
"port": 6053,
|
||||
"hostname": "tube_zb_gw_cc2652p2_poe.local.",
|
||||
"properties": {"address": "tube_zb_gw_cc2652p2_poe.local"},
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"zha", context={"source": SOURCE_ZEROCONF}, data=service_info
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data[CONF_DEVICE] == {
|
||||
CONF_DEVICE_PATH: "socket://192.168.1.22:6638",
|
||||
}
|
||||
|
||||
|
||||
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
|
||||
async def test_discovery_via_usb(detect_mock, hass):
|
||||
"""Test usb flow -- radio detected."""
|
||||
@@ -317,6 +345,37 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass):
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
|
||||
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
|
||||
async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass):
|
||||
"""Test usb flow that was ignored gets updated."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
data={},
|
||||
unique_id="AAAA:AAAA_1234_test_zigbee radio",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
discovery_info = {
|
||||
"device": "/dev/ttyZIGBEE",
|
||||
"pid": "AAAA",
|
||||
"vid": "AAAA",
|
||||
"serial_number": "1234",
|
||||
"description": "zigbee radio",
|
||||
"manufacturer": "test",
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"zha", context={"source": SOURCE_USB}, data=discovery_info
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data[CONF_DEVICE] == {
|
||||
CONF_DEVICE_PATH: "/dev/ttyZIGBEE",
|
||||
}
|
||||
|
||||
|
||||
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
|
||||
@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
|
||||
async def test_discovery_already_setup(detect_mock, hass):
|
||||
|
||||
@@ -338,7 +338,7 @@ async def test_add_node_secure(
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.begin_inclusion",
|
||||
"options": {"inclusionStrategy": InclusionStrategy.SECURITY_S0},
|
||||
"options": {"strategy": InclusionStrategy.SECURITY_S0},
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
@@ -363,7 +363,7 @@ async def test_add_node(
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.begin_inclusion",
|
||||
"options": {"inclusionStrategy": InclusionStrategy.INSECURE},
|
||||
"options": {"strategy": InclusionStrategy.INSECURE},
|
||||
}
|
||||
|
||||
event = Event(
|
||||
@@ -671,7 +671,7 @@ async def test_replace_failed_node_secure(
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.replace_failed_node",
|
||||
"nodeId": nortek_thermostat.node_id,
|
||||
"options": {"inclusionStrategy": InclusionStrategy.SECURITY_S0},
|
||||
"options": {"strategy": InclusionStrategy.SECURITY_S0},
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
@@ -720,7 +720,7 @@ async def test_replace_failed_node(
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.replace_failed_node",
|
||||
"nodeId": nortek_thermostat.node_id,
|
||||
"options": {"inclusionStrategy": InclusionStrategy.INSECURE},
|
||||
"options": {"strategy": InclusionStrategy.INSECURE},
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
@@ -756,10 +756,7 @@ async def test_usb_discovery_already_running(hass, supervisor, addon_running):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"discovery_info",
|
||||
[
|
||||
NORTEK_ZIGBEE_DISCOVERY_INFO,
|
||||
CP2652_ZIGBEE_DISCOVERY_INFO,
|
||||
],
|
||||
[CP2652_ZIGBEE_DISCOVERY_INFO],
|
||||
)
|
||||
async def test_abort_usb_discovery_aborts_specific_devices(
|
||||
hass, supervisor, addon_options, discovery_info
|
||||
|
||||
@@ -32,7 +32,7 @@ async def test_caching_data(hass):
|
||||
await data.store.async_save([state.as_dict() for state in stored_states])
|
||||
|
||||
# Emulate a fresh load
|
||||
hass.data[DATA_RESTORE_STATE_TASK] = None
|
||||
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||
|
||||
entity = RestoreEntity()
|
||||
entity.hass = hass
|
||||
@@ -59,7 +59,7 @@ async def test_periodic_write(hass):
|
||||
await data.store.async_save([])
|
||||
|
||||
# Emulate a fresh load
|
||||
hass.data[DATA_RESTORE_STATE_TASK] = None
|
||||
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||
|
||||
entity = RestoreEntity()
|
||||
entity.hass = hass
|
||||
@@ -105,7 +105,7 @@ async def test_save_persistent_states(hass):
|
||||
await data.store.async_save([])
|
||||
|
||||
# Emulate a fresh load
|
||||
hass.data[DATA_RESTORE_STATE_TASK] = None
|
||||
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||
|
||||
entity = RestoreEntity()
|
||||
entity.hass = hass
|
||||
@@ -170,7 +170,8 @@ async def test_hass_starting(hass):
|
||||
await data.store.async_save([state.as_dict() for state in stored_states])
|
||||
|
||||
# Emulate a fresh load
|
||||
hass.data[DATA_RESTORE_STATE_TASK] = None
|
||||
hass.state = CoreState.not_running
|
||||
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||
|
||||
entity = RestoreEntity()
|
||||
entity.hass = hass
|
||||
|
||||
@@ -12,29 +12,33 @@ def mock_hass():
|
||||
return Mock(data={})
|
||||
|
||||
|
||||
async def test_singleton_async(mock_hass):
|
||||
@pytest.mark.parametrize("result", (object(), {}, []))
|
||||
async def test_singleton_async(mock_hass, result):
|
||||
"""Test singleton with async function."""
|
||||
|
||||
@singleton.singleton("test_key")
|
||||
async def something(hass):
|
||||
return object()
|
||||
return result
|
||||
|
||||
result1 = await something(mock_hass)
|
||||
result2 = await something(mock_hass)
|
||||
assert result1 is result
|
||||
assert result1 is result2
|
||||
assert "test_key" in mock_hass.data
|
||||
assert mock_hass.data["test_key"] is result1
|
||||
|
||||
|
||||
def test_singleton(mock_hass):
|
||||
@pytest.mark.parametrize("result", (object(), {}, []))
|
||||
def test_singleton(mock_hass, result):
|
||||
"""Test singleton with function."""
|
||||
|
||||
@singleton.singleton("test_key")
|
||||
def something(hass):
|
||||
return object()
|
||||
return result
|
||||
|
||||
result1 = something(mock_hass)
|
||||
result2 = something(mock_hass)
|
||||
assert result1 is result
|
||||
assert result1 is result2
|
||||
assert "test_key" in mock_hass.data
|
||||
assert mock_hass.data["test_key"] is result1
|
||||
|
||||
@@ -27,14 +27,23 @@ async def apply_stop_hass(stop_hass):
|
||||
"""Make sure all hass are stopped."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_is_file():
|
||||
"""Mock is_file."""
|
||||
# All files exist except for the old entity registry file
|
||||
with patch(
|
||||
"os.path.isfile", lambda path: not path.endswith("entity_registry.yaml")
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def normalize_yaml_files(check_dict):
|
||||
"""Remove configuration path from ['yaml_files']."""
|
||||
root = get_test_config_dir()
|
||||
return [key.replace(root, "...") for key in sorted(check_dict["yaml_files"].keys())]
|
||||
|
||||
|
||||
@patch("os.path.isfile", return_value=True)
|
||||
def test_bad_core_config(isfile_patch, loop):
|
||||
def test_bad_core_config(mock_is_file, loop):
|
||||
"""Test a bad core config setup."""
|
||||
files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG}
|
||||
with patch_yaml_files(files):
|
||||
@@ -43,8 +52,7 @@ def test_bad_core_config(isfile_patch, loop):
|
||||
assert res["except"]["homeassistant"][1] == {"unit_system": "bad"}
|
||||
|
||||
|
||||
@patch("os.path.isfile", return_value=True)
|
||||
def test_config_platform_valid(isfile_patch, loop):
|
||||
def test_config_platform_valid(mock_is_file, loop):
|
||||
"""Test a valid platform setup."""
|
||||
files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"}
|
||||
with patch_yaml_files(files):
|
||||
@@ -57,8 +65,7 @@ def test_config_platform_valid(isfile_patch, loop):
|
||||
assert len(res["yaml_files"]) == 1
|
||||
|
||||
|
||||
@patch("os.path.isfile", return_value=True)
|
||||
def test_component_platform_not_found(isfile_patch, loop):
|
||||
def test_component_platform_not_found(mock_is_file, loop):
|
||||
"""Test errors if component or platform not found."""
|
||||
# Make sure they don't exist
|
||||
files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"}
|
||||
@@ -89,8 +96,7 @@ def test_component_platform_not_found(isfile_patch, loop):
|
||||
assert len(res["yaml_files"]) == 1
|
||||
|
||||
|
||||
@patch("os.path.isfile", return_value=True)
|
||||
def test_secrets(isfile_patch, loop):
|
||||
def test_secrets(mock_is_file, loop):
|
||||
"""Test secrets config checking method."""
|
||||
secrets_path = get_test_config_dir("secrets.yaml")
|
||||
|
||||
@@ -121,8 +127,7 @@ def test_secrets(isfile_patch, loop):
|
||||
]
|
||||
|
||||
|
||||
@patch("os.path.isfile", return_value=True)
|
||||
def test_package_invalid(isfile_patch, loop):
|
||||
def test_package_invalid(mock_is_file, loop):
|
||||
"""Test an invalid package."""
|
||||
files = {
|
||||
YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]')
|
||||
|
||||
@@ -56,11 +56,14 @@ async def test_home_assistant_core_config_validation(hass):
|
||||
assert result is None
|
||||
|
||||
|
||||
async def test_async_enable_logging(hass):
|
||||
async def test_async_enable_logging(hass, caplog):
|
||||
"""Test to ensure logging is migrated to the queue handlers."""
|
||||
with patch("logging.getLogger"), patch(
|
||||
"homeassistant.bootstrap.async_activate_log_queue_handler"
|
||||
) as mock_async_activate_log_queue_handler:
|
||||
) as mock_async_activate_log_queue_handler, patch(
|
||||
"homeassistant.bootstrap.logging.handlers.RotatingFileHandler.doRollover",
|
||||
side_effect=OSError,
|
||||
):
|
||||
bootstrap.async_enable_logging(hass)
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
mock_async_activate_log_queue_handler.reset_mock()
|
||||
@@ -75,6 +78,8 @@ async def test_async_enable_logging(hass):
|
||||
for f in glob.glob("testing_config/home-assistant.log*"):
|
||||
os.remove(f)
|
||||
|
||||
assert "Error rolling over log file" in caplog.text
|
||||
|
||||
|
||||
async def test_load_hassio(hass):
|
||||
"""Test that we load Hass.io component."""
|
||||
|
||||
Reference in New Issue
Block a user