Compare commits

..

58 Commits

Author SHA1 Message Date
Paulus Schoutsen ddb0a6092f Merge pull request #56116 from home-assistant/rc 2021-09-11 13:02:14 -07:00
Paulus Schoutsen 292c05ab9f Bumped version to 2021.9.6 2021-09-11 12:02:44 -07:00
ehendrix23 a900c02c10 Bump pymyq to 3.1.4 (#56089) 2021-09-11 12:02:39 -07:00
Paulus Schoutsen 0cfbff3ff9 Fix singleton not working with falsey values (#56072) 2021-09-11 12:02:38 -07:00
Sean Vig ee892beceb Bump amcrest version to 1.8.1 (#56058)
The current version of the `amcrest` package has a bug in exposing if
the video stream is enabled, which leads to the substream status being
used to set if the camera is on or off.  The updated version of
`amcrest` fixes this bug.

Fixes #55661
2021-09-11 12:02:37 -07:00
micha91 fe713b943f Fix UDP message handling by upgrading aiomusiccast to 0.9.2 (#56041) 2021-09-11 12:02:36 -07:00
Erik Montnemery 2079956350 Suppress last_reset deprecation warning for energy cost sensor (#56037) 2021-09-11 12:02:36 -07:00
Erik Montnemery c424f99aab Correct confusing log message in sensor statistics (#56016) 2021-09-11 12:02:35 -07:00
Paulus Schoutsen 5cc54618c5 Merge pull request #55969 from home-assistant/rc 2021-09-08 13:42:09 -07:00
Erik Montnemery 8f344252c4 Add significant change support to AQI type sensors (#55833) 2021-09-08 12:47:59 -07:00
Erik Montnemery cbe4b2dc1d Add support for state class measurement to energy cost sensor (#55962) 2021-09-08 12:46:43 -07:00
Paulus Schoutsen a17d2d7c71 Fix gas validation (#55886) 2021-09-08 12:45:41 -07:00
Ruslan Sayfutdinov e3815c6c2e Pin setuptools<58 2021-09-08 12:04:32 -07:00
Paulus Schoutsen 5cba7932f3 Bumped version to 2021.9.5 2021-09-08 08:22:38 -07:00
Erik Montnemery 413430bdba Fix handling of imperial units in long term statistics (#55959) 2021-09-08 08:22:34 -07:00
Erik Montnemery 81462d8655 Do not allow inf or nan sensor states in statistics (#55943) 2021-09-08 08:22:33 -07:00
Erik Montnemery 8ee4b49aa9 Do not let one bad statistic spoil the bunch (#55942) 2021-09-08 08:22:32 -07:00
Shay Levy 19d7cb4439 Bump aioswitcher to 2.0.5 (#55934) 2021-09-08 08:22:31 -07:00
Raman Gupta 21ebf4f3e6 Allow multiple template.select platform entries (#55908) 2021-09-08 08:22:31 -07:00
Maciej Bieniek 980fcef36f Fix available property for Xiaomi Miio fan platform (#55889)
* Fix available

* Suggested change
2021-09-08 08:22:30 -07:00
Diogo Gomes e7fd24eade Integration Sensor Initial State (#55875)
* initial state is UNAVAILABLE

* update tests
2021-09-08 08:22:08 -07:00
Pascal Winters 9ecb75dc70 Edit unit of measurement for gas/electricity supplier prices (#55771)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-09-08 08:20:49 -07:00
RDFurman 7f3adce675 Try to avoid rate limiting in honeywell (#55304)
* Limit parallel update and sleep loop

* Use asyncio sleep instead

* Extract sleep to const for testing

* Make loop sleep 0 in test
2021-09-08 08:20:48 -07:00
Paulus Schoutsen f0649855f9 Merge pull request #55870 from home-assistant/rc 2021-09-06 16:05:08 -07:00
Paulus Schoutsen 823c3735ce Bumped version to 2021.9.4 2021-09-06 13:41:39 -07:00
Diogo Gomes 68131a5c00 Integration Sensor unit of measurement overwrite (#55869) 2021-09-06 13:41:33 -07:00
J. Nick Koston be0f767c34 Fix exception during rediscovery of ignored zha config entries (#55859)
Fixes #55709
2021-09-06 13:41:33 -07:00
Maciej Bieniek 450652a501 Fix target humidity step for Xiaomi MJJSQ humidifiers (#55858) 2021-09-06 13:41:32 -07:00
Daniel Hjelseth Høyer 8523f569c0 Surepetcare, bug fix (#55842) 2021-09-06 13:41:31 -07:00
mrwhite31 5f289434d3 Fix typo in in rfxtrx Barometer sensor (#55839)
Fix typo in sensor.py to fix barometer unavailability
2021-09-06 13:41:31 -07:00
Maciej Bieniek 3df6dfecab Fix a lazy preset mode update for Xiaomi Miio fans (#55837) 2021-09-06 13:41:30 -07:00
Martin Hjelmare d6eda65302 Bump zwave-js-server-python to 0.30.0 (#55831) 2021-09-06 13:41:29 -07:00
Brandon Rothweiler 7a5bc2784a Upgrade pymazda to 0.2.1 (#55820) 2021-09-06 13:41:28 -07:00
David Bonnes 00878467cc Fix incomfort min/max temperatures (#55806) 2021-09-06 13:41:28 -07:00
Maciej Bieniek 899d8164b0 Fix xiaomi miio Air Quality Monitor initialization (#55773) 2021-09-06 13:41:27 -07:00
jan iversen 823fd60991 Allow same address different register types in modbus (#55767) 2021-09-06 13:41:26 -07:00
jan iversen a6bb0eadca Allow same IP if ports are different on modbus (#55766) 2021-09-06 13:41:25 -07:00
Tatham Oddie eb70354ee7 Fix logbook entity_matches_only query mode (#55761)
The string matching template needs to match the same compact JSON format
as the data is now written in.
2021-09-06 13:41:25 -07:00
Joshi bd53185bed Fix switch name attribute for thinkingcleaner (#55730) 2021-09-06 13:41:24 -07:00
Paulus Schoutsen df9a899bbd Merge pull request #55753 from home-assistant/rc 2021-09-04 14:51:15 -07:00
Paulus Schoutsen 37cf295e20 Bumped version to 2021.9.3 2021-09-04 14:13:37 -07:00
Simone Chemelli 04816fe26d Fix SamsungTV sendkey when not connected (#55723) 2021-09-04 14:13:34 -07:00
Anders Melchiorsen eb48e75fc5 Fix LIFX firmware version information (#55713) 2021-09-04 14:13:33 -07:00
Simone Chemelli 9d5431fba1 Handle Fritz InternalError (#55711) 2021-09-04 14:13:32 -07:00
Erik Montnemery a4f2c5583d Handle negative numbers in sensor long term statistics (#55708)
* Handle negative numbers in sensor long term statistics

* Use negative states in tests
2021-09-04 14:13:32 -07:00
Paulus Schoutsen a37c3af2b4 better detect legacy eagly devices (#55706) 2021-09-04 14:13:31 -07:00
Paulus Schoutsen 33047d7260 Merge pull request #55673 from home-assistant/rc 2021-09-03 10:53:16 -07:00
Paulus Schoutsen e3405d226a Bumped version to 2021.9.2 2021-09-03 10:16:36 -07:00
Paulus Schoutsen 3008ff03b2 Guard for doRollover failing (#55669) 2021-09-03 10:16:31 -07:00
Joakim Sørensen 8592d94a3c Fix hdmi_cec switches (#55666) 2021-09-03 10:16:30 -07:00
Nikolay Vasilchuk b36e86d95c Fix Starline sensor state AttributeError (#55654)
* Fix starline sensors state

* Black
2021-09-03 10:16:29 -07:00
Paulus Schoutsen f61a1ecae7 Guard for unexpected exceptions in device automation (#55639)
* Guard for unexpected exceptions in device automation

* merge

Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-03 10:16:28 -07:00
Paulus Schoutsen 80c074ca82 Better handle invalid trigger config (#55637) 2021-09-03 10:16:28 -07:00
Paulus Schoutsen ff91ff4cd2 Fix template sensor availability (#55635) 2021-09-03 10:16:27 -07:00
J. Nick Koston 93c2a7dd70 Narrow zwave_js USB discovery (#55613)
- Avoid triggering discovery when we can know in advance the
  device is not a Z-Wave stick
2021-09-03 10:16:26 -07:00
Michael da3ee9ed4b Fix CONFIG_SCHEMA validation in Speedtest.net (#55612) 2021-09-03 10:16:25 -07:00
Pascal Vizeli 2ef607651d Disable observer for USB on containers (#55570)
* Disable observer for USB on containers

* remove operating system test

Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-03 10:16:24 -07:00
J. Nick Koston 88ca83a30b Ignore missing devices when in ssdp unsee (#55553) 2021-09-03 10:16:24 -07:00
80 changed files with 1506 additions and 502 deletions
+1 -1
View File
@@ -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 .
+5 -1
View File
@@ -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",
+38 -14
View File
@@ -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."""
+80 -20
View File
@@ -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
+7 -1
View File
@@ -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):
+12 -3
View File
@@ -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
+10 -7
View File
@@ -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()
+10 -6
View File
@@ -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:
+16 -17
View File
@@ -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):
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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": ?"([^"]+)"')
+1 -1
View File
@@ -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"
+10 -1
View File
@@ -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)
+1 -1
View File
@@ -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(
{
+1 -1
View File
@@ -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,
+4 -2
View File
@@ -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 "
+22 -16
View File
@@ -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))]),
}
)
},
+1 -1
View File
@@ -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."""
+2 -1
View File
@@ -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
}
+13 -13
View File
@@ -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."""
+1 -1
View File
@@ -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"]
+14 -15
View File
@@ -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
+32 -27
View File
@@ -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": [
{
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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"]}
]
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+2 -1
View File
@@ -32,7 +32,8 @@ USB = [
{
"domain": "zwave_js",
"vid": "10C4",
"pid": "8A2A"
"pid": "8A2A",
"description": "*z-wave*"
},
{
"domain": "zwave_js",
+30 -40
View File
@@ -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
+40 -10
View File
@@ -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:
+8 -12
View File
@@ -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)
+4
View File
@@ -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
+7 -7
View File
@@ -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
+5 -5
View File
@@ -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
):
+282 -6
View File
@@ -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
+57 -10
View File
@@ -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": [],
}
+4
View File
@@ -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:
+3 -4
View File
@@ -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
+39
View File
@@ -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)
+111 -8
View File
@@ -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)
+114 -41
View File
@@ -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³", 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", "", "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", "", "", 1),
(METRIC_SYSTEM, "gas", "ft³", "", 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)
+110 -3
View File
@@ -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"]
)
+8 -4
View File
@@ -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
+1 -1
View File
@@ -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()
+34 -2
View File
@@ -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
+42
View File
@@ -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"
-49
View File
@@ -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",
+59
View File
@@ -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):
+4 -4
View File
@@ -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
+5 -4
View File
@@ -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
+8 -4
View File
@@ -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
+15 -10
View File
@@ -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"]')
+7 -2
View File
@@ -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."""