Compare commits

..

16 Commits

Author SHA1 Message Date
Paulus Schoutsen
74357d9760 Bumped version to 2021.4.0b2 2021-04-01 23:33:37 +00:00
Erik Montnemery
231a55d416 Include blueprint input in automation trace (#48575) 2021-04-01 23:33:04 +00:00
Erik Montnemery
e760c23f37 Include script script_execution in script and automation traces (#48576) 2021-04-01 23:32:47 +00:00
Paulus Schoutsen
39f68de5fa Bumped version to 2021.4.0b1 2021-04-01 23:23:47 +00:00
Robert Svensson
68b189cf9f Increase time out for http requests done in Axis integration (#48610) 2021-04-01 23:23:31 +00:00
Bram Kragten
8d0941ba65 Update frontend to 20210402.0 (#48609) 2021-04-01 23:23:29 +00:00
Paulus Schoutsen
d1a48c7c5c Clean up mobile app (#48607)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-04-01 23:23:28 +00:00
Franck Nijhof
f0f8b79be0 Fix websocket search for related (#48603)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 23:23:26 +00:00
Franck Nijhof
c2d17a72b7 Allow templatable service target to support scripts (#48600) 2021-04-01 23:23:25 +00:00
Franck Nijhof
f0bd3c577f Upgrade numpy to 1.20.2 (#48597) 2021-04-01 23:23:24 +00:00
Erik Montnemery
947ac514b9 Return config entry details for 1-step config flows (#48585) 2021-04-01 23:23:22 +00:00
epenet
5df90b32fc Cleanup orphan devices in onewire integration (#48581)
* Cleanup orphan devices (https://github.com/home-assistant/core/issues/47438)

* Refactor unit testing

* Filter device entries for this config entry

* Update logging

* Cleanup check
2021-04-01 23:23:20 +00:00
Robert Svensson
f08e7dccdf Don't care about DPI entries when looking for clients to be restored from UniFi (#48579)
* DPI switches shouldnt be restored, they're not part of clients to be restored

* Only care about Block and POE switch entries
2021-04-01 23:23:20 +00:00
Aaron Bach
3982849275 Fix incorrect constant import in Ambient PWS (#48574) 2021-04-01 23:23:17 +00:00
Joakim Sørensen
07827ca55d Remove analytics from default_config (#48566) 2021-04-01 23:23:16 +00:00
youknowjack0
16da181692 Fix timer.finish to cancel callback (#48549)
Timer.finish doesn't cancel the callback, which can lead to incorrect early cancellation of the timer if it is subsequently restarted. 

Bug reported here: https://community.home-assistant.io/t/timer-component-timer-stops-before-time-is-up/96038
2021-04-01 23:23:15 +00:00
44 changed files with 868 additions and 501 deletions

View File

@@ -1,6 +1,5 @@
"""Support for Ambient Weather Station sensors."""
from homeassistant.components.binary_sensor import DOMAIN as SENSOR
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback

View File

@@ -57,6 +57,7 @@ from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trace import (
TraceElement,
script_execution_set,
trace_append_element,
trace_get,
trace_path,
@@ -272,6 +273,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
variables,
trigger_variables,
raw_config,
blueprint_inputs,
):
"""Initialize an automation entity."""
self._id = automation_id
@@ -289,6 +291,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._variables: ScriptVariables = variables
self._trigger_variables: ScriptVariables = trigger_variables
self._raw_config = raw_config
self._blueprint_inputs = blueprint_inputs
@property
def name(self):
@@ -436,7 +439,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
trigger_context = Context(parent_id=parent_id)
with trace_automation(
self.hass, self.unique_id, self._raw_config, trigger_context
self.hass,
self.unique_id,
self._raw_config,
self._blueprint_inputs,
trigger_context,
) as automation_trace:
if self._variables:
try:
@@ -471,6 +478,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
"Conditions not met, aborting automation. Condition summary: %s",
trace_get(clear=False),
)
script_execution_set("failed_conditions")
return
self.async_set_context(trigger_context)
@@ -601,10 +609,12 @@ async def _async_process_config(
]
for list_no, config_block in enumerate(conf):
raw_blueprint_inputs = None
raw_config = None
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
blueprints_used = True
blueprint_inputs = config_block
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
try:
raw_config = blueprint_inputs.async_substitute()
@@ -673,6 +683,7 @@ async def _async_process_config(
variables,
config_block.get(CONF_TRIGGER_VARIABLES),
raw_config,
raw_blueprint_inputs,
)
entities.append(entity)

View File

@@ -18,11 +18,12 @@ class AutomationTrace(ActionTrace):
self,
item_id: str,
config: dict[str, Any],
blueprint_inputs: dict[str, Any],
context: Context,
):
"""Container for automation trace."""
key = ("automation", item_id)
super().__init__(key, config, context)
super().__init__(key, config, blueprint_inputs, context)
self._trigger_description: str | None = None
def set_trigger_description(self, trigger: str) -> None:
@@ -37,9 +38,9 @@ class AutomationTrace(ActionTrace):
@contextmanager
def trace_automation(hass, automation_id, config, context):
def trace_automation(hass, automation_id, config, blueprint_inputs, context):
"""Trace action execution of automation with automation_id."""
trace = AutomationTrace(automation_id, config, context)
trace = AutomationTrace(automation_id, config, blueprint_inputs, context)
async_store_trace(hass, trace)
try:

View File

@@ -304,7 +304,7 @@ async def get_device(hass, host, port, username, password):
)
try:
with async_timeout.timeout(15):
with async_timeout.timeout(30):
await device.vapix.initialize()
return device

View File

@@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==43"],
"requirements": ["axis==44"],
"dhcp": [
{ "hostname": "axis-00408c*", "macaddress": "00408C*" },
{ "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" },

View File

@@ -97,6 +97,17 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView):
return self.json({"require_restart": not result})
def _prepare_config_flow_result_json(result, prepare_result_json):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return prepare_result_json(result)
data = result.copy()
data["result"] = entry_json(result["result"])
data.pop("data")
return data
class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
@@ -118,13 +129,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data["result"] = data["result"].entry_id
data.pop("data")
return data
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerFlowResourceView(FlowManagerResourceView):
@@ -151,13 +156,7 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return super()._prepare_result_json(result)
data = result.copy()
data["result"] = entry_json(result["result"])
data.pop("data")
return data
return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerAvailableFlowView(HomeAssistantView):

View File

@@ -3,7 +3,6 @@
"name": "Default Config",
"documentation": "https://www.home-assistant.io/integrations/default_config",
"dependencies": [
"analytics",
"automation",
"cloud",
"counter",

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20210331.0"
"home-assistant-frontend==20210402.0"
],
"dependencies": [
"api",

View File

@@ -3,6 +3,6 @@
"name": "IQVIA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
"requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"],
"requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"],
"codeowners": ["@bachya"]
}

View File

@@ -1,6 +1,4 @@
"""Binary sensor platform for mobile_app."""
from functools import partial
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON
from homeassistant.core import callback
@@ -48,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
@callback
def handle_sensor_registration(webhook_id, data):
def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
@@ -66,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
partial(handle_sensor_registration, webhook_id),
handle_sensor_registration,
)

View File

@@ -34,13 +34,14 @@ class MobileAppEntity(RestoreEntity):
self._registration = entry.data
self._unique_id = config[CONF_UNIQUE_ID]
self._entity_type = config[ATTR_SENSOR_TYPE]
self.unsub_dispatcher = None
self._name = config[CONF_NAME]
async def async_added_to_hass(self):
"""Register callbacks."""
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
)
)
state = await self.async_get_last_state()
@@ -49,11 +50,6 @@ class MobileAppEntity(RestoreEntity):
self.async_restore_last_state(state)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
@callback
def async_restore_last_state(self, last_state):
"""Restore previous state."""

View File

@@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO):
async def async_get_service(hass, config, discovery_info=None):
"""Get the mobile_app notification service."""
session = async_get_clientsession(hass)
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session)
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass)
return service
class MobileAppNotificationService(BaseNotificationService):
"""Implement the notification service for mobile_app."""
def __init__(self, session):
def __init__(self, hass):
"""Initialize the service."""
self._session = session
self._hass = hass
@property
def targets(self):
@@ -141,7 +140,9 @@ class MobileAppNotificationService(BaseNotificationService):
try:
with async_timeout.timeout(10):
response = await self._session.post(push_url, json=data)
response = await async_get_clientsession(self._hass).post(
push_url, json=data
)
result = await response.json()
if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]:

View File

@@ -1,6 +1,4 @@
"""Sensor platform for mobile_app."""
from functools import partial
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID
from homeassistant.core import callback
@@ -50,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
@callback
def handle_sensor_registration(webhook_id, data):
def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
@@ -68,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
partial(handle_sensor_registration, webhook_id),
handle_sensor_registration,
)

View File

@@ -472,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data):
device_name = config_entry.data[ATTR_DEVICE_NAME]
resp = {}
for sensor in data:
entity_type = sensor[ATTR_SENSOR_TYPE]
@@ -495,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]}
try:
sensor = sensor_schema_full(sensor)
except vol.Invalid as err:
@@ -513,9 +512,8 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
new_state = {**entry, **sensor}
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor)
resp[unique_id] = {"success": True}

View File

@@ -1,13 +1,17 @@
"""The 1-Wire component."""
import asyncio
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN, PLATFORMS
from .onewirehub import CannotConnect, OneWireHub
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up 1-Wire integrations."""
@@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
hass.data[DOMAIN][config_entry.unique_id] = onewirehub
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
async def cleanup_registry() -> None:
# Get registries
device_registry, entity_registry = await asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
)
# Generate list of all device entries
registry_devices = [
entry.id
for entry in dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
]
# Remove devices that don't belong to any entity
for device_id in registry_devices:
if not er.async_entries_for_device(
entity_registry, device_id, include_disabled_entities=True
):
_LOGGER.debug(
"Removing device `%s` because it does not have any entities",
device_id,
)
device_registry.async_remove_device(device_id)
async def start_platforms() -> None:
"""Start platforms and cleanup devices."""
# wait until all required platforms are ready
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_setup(config_entry, platform)
for platform in PLATFORMS
]
)
await cleanup_registry()
hass.async_create_task(start_platforms())
return True

View File

@@ -2,6 +2,6 @@
"domain": "opencv",
"name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/opencv",
"requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"],
"requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"],
"codeowners": []
}

View File

@@ -19,7 +19,7 @@ class ScriptTrace(ActionTrace):
):
"""Container for automation trace."""
key = ("script", item_id)
super().__init__(key, config, context)
super().__init__(key, config, None, context)
@contextmanager

View File

@@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "search/related",
@@ -38,6 +37,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
vol.Required("item_id"): str,
}
)
@callback
def websocket_search_related(hass, connection, msg):
"""Handle search."""
searcher = Searcher(

View File

@@ -6,7 +6,7 @@
"tensorflow==2.3.0",
"tf-models-official==2.3.0",
"pycocotools==2.0.1",
"numpy==1.19.2",
"numpy==1.20.2",
"pillow==8.1.2"
],
"codeowners": []

View File

@@ -327,7 +327,9 @@ class Timer(RestoreEntity):
if self._state != STATUS_ACTIVE:
return
self._listener = None
if self._listener:
self._listener()
self._listener = None
self._state = STATUS_IDLE
self._end = None
self._remaining = None

View File

@@ -8,6 +8,7 @@ from typing import Any, Deque
from homeassistant.core import Context
from homeassistant.helpers.trace import (
TraceElement,
script_execution_get,
trace_id_get,
trace_id_set,
trace_set_child_id,
@@ -47,14 +48,17 @@ class ActionTrace:
self,
key: tuple[str, str],
config: dict[str, Any],
blueprint_inputs: dict[str, Any],
context: Context,
):
"""Container for script trace."""
self._trace: dict[str, Deque[TraceElement]] | None = None
self._config: dict[str, Any] = config
self._blueprint_inputs: dict[str, Any] = blueprint_inputs
self.context: Context = context
self._error: Exception | None = None
self._state: str = "running"
self._script_execution: str | None = None
self.run_id: str = str(next(self._run_ids))
self._timestamp_finish: dt.datetime | None = None
self._timestamp_start: dt.datetime = dt_util.utcnow()
@@ -75,6 +79,7 @@ class ActionTrace:
"""Set finish time."""
self._timestamp_finish = dt_util.utcnow()
self._state = "stopped"
self._script_execution = script_execution_get()
def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this ActionTrace."""
@@ -90,6 +95,7 @@ class ActionTrace:
{
"trace": traces,
"config": self._config,
"blueprint_inputs": self._blueprint_inputs,
"context": self.context,
}
)
@@ -109,6 +115,7 @@ class ActionTrace:
"last_step": last_step,
"run_id": self.run_id,
"state": self._state,
"script_execution": self._script_execution,
"timestamp": {
"start": self._timestamp_start,
"finish": self._timestamp_finish,

View File

@@ -2,7 +2,7 @@
"domain": "trend",
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
"requirements": ["numpy==1.19.2"],
"requirements": ["numpy==1.20.2"],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -29,6 +29,7 @@ import async_timeout
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
CONF_HOST,
@@ -347,7 +348,10 @@ class UniFiController:
):
if entry.domain == TRACKER_DOMAIN:
mac = entry.unique_id.split("-", 1)[0]
elif entry.domain == SWITCH_DOMAIN:
elif entry.domain == SWITCH_DOMAIN and (
entry.unique_id.startswith(BLOCK_SWITCH)
or entry.unique_id.startswith(POE_SWITCH)
):
mac = entry.unique_id.split("-", 1)[1]
else:
continue

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
MINOR_VERSION = 4
PATCH_VERSION = "0b0"
PATCH_VERSION = "0b2"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0)

View File

@@ -916,7 +916,7 @@ SERVICE_SCHEMA = vol.All(
vol.Optional("data"): vol.All(dict, template_complex),
vol.Optional("data_template"): vol.All(dict, template_complex),
vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS,
vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template),
}
),
has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE),

View File

@@ -63,6 +63,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.event import async_call_later, async_track_template
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.trace import script_execution_set
from homeassistant.helpers.trigger import (
async_initialize_triggers,
async_validate_trigger_config,
@@ -332,15 +333,19 @@ class _ScriptRun:
async def async_run(self) -> None:
"""Run script."""
try:
if self._stop.is_set():
return
self._log("Running %s", self._script.running_description)
for self._step, self._action in enumerate(self._script.sequence):
if self._stop.is_set():
script_execution_set("cancelled")
break
await self._async_step(log_exceptions=False)
else:
script_execution_set("finished")
except _StopScript:
pass
script_execution_set("aborted")
except Exception:
script_execution_set("error")
raise
finally:
self._finish()
@@ -1137,6 +1142,7 @@ class Script:
if self.script_mode == SCRIPT_MODE_SINGLE:
if self._max_exceeded != "SILENT":
self._log("Already running", level=LOGSEVERITY[self._max_exceeded])
script_execution_set("failed_single")
return
if self.script_mode == SCRIPT_MODE_RESTART:
self._log("Restarting")
@@ -1147,6 +1153,7 @@ class Script:
"Maximum number of runs exceeded",
level=LOGSEVERITY[self._max_exceeded],
)
script_execution_set("failed_max_runs")
return
# If this is a top level Script then make a copy of the variables in case they

View File

@@ -204,10 +204,15 @@ def async_prepare_call_from_config(
target = {}
if CONF_TARGET in config:
conf = config.get(CONF_TARGET)
conf = config[CONF_TARGET]
try:
template.attach(hass, conf)
target.update(template.render_complex(conf, variables))
if isinstance(conf, template.Template):
conf.hass = hass
target.update(conf.async_render(variables))
else:
template.attach(hass, conf)
target.update(template.render_complex(conf, variables))
if CONF_ENTITY_ID in target:
target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID])
except TemplateError as ex:

View File

@@ -88,6 +88,10 @@ variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None)
trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar(
"trace_id_cv", default=None
)
# Reason for stopped script execution
script_execution_cv: ContextVar[StopReason | None] = ContextVar(
"script_execution_cv", default=None
)
def trace_id_set(trace_id: tuple[str, str]) -> None:
@@ -172,6 +176,7 @@ def trace_clear() -> None:
trace_stack_cv.set(None)
trace_path_stack_cv.set(None)
variables_cv.set(None)
script_execution_cv.set(StopReason())
def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None:
@@ -187,6 +192,28 @@ def trace_set_result(**kwargs: Any) -> None:
node.set_result(**kwargs)
class StopReason:
"""Mutable container class for script_execution."""
script_execution: str | None = None
def script_execution_set(reason: str) -> None:
"""Set stop reason."""
data = script_execution_cv.get()
if data is None:
return
data.script_execution = reason
def script_execution_get() -> str | None:
"""Return the current trace."""
data = script_execution_cv.get()
if data is None:
return None
return data.script_execution
@contextmanager
def trace_path(suffix: str | list[str]) -> Generator:
"""Go deeper in the config tree.

View File

@@ -16,7 +16,7 @@ defusedxml==0.6.0
distro==1.5.0
emoji==1.2.0
hass-nabucasa==0.42.0
home-assistant-frontend==20210331.0
home-assistant-frontend==20210402.0
httpx==0.17.1
jinja2>=2.11.3
netdisco==2.8.2

View File

@@ -11,7 +11,7 @@ import traceback
from typing import Any, Awaitable, Callable, Coroutine, cast, overload
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback, is_callback
class HideSensitiveDataFilter(logging.Filter):
@@ -138,6 +138,7 @@ def catch_log_exception(
log_exception(format_err, *args)
wrapper_func = async_wrapper
else:
@wraps(func)
@@ -148,6 +149,9 @@ def catch_log_exception(
except Exception: # pylint: disable=broad-except
log_exception(format_err, *args)
if is_callback(check_func):
wrapper = callback(wrapper)
wrapper_func = wrapper
return wrapper_func

View File

@@ -313,7 +313,7 @@ av==8.0.3
# avion==0.10
# homeassistant.components.axis
axis==43
axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210331.0
home-assistant-frontend==20210402.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -1019,7 +1019,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
numpy==1.19.2
numpy==1.20.2
# homeassistant.components.oasa_telematics
oasatelematics==0.3

View File

@@ -187,7 +187,7 @@ auroranoaa==0.0.2
av==8.0.3
# homeassistant.components.axis
axis==43
axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210331.0
home-assistant-frontend==20210402.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -531,7 +531,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
numpy==1.19.2
numpy==1.20.2
# homeassistant.components.google
oauth2client==4.0.0

View File

@@ -320,7 +320,17 @@ async def test_create_account(hass, client):
"title": "Test Entry",
"type": "create_entry",
"version": 1,
"result": entries[0].entry_id,
"result": {
"connection_class": "unknown",
"disabled_by": None,
"domain": "test",
"entry_id": entries[0].entry_id,
"source": "user",
"state": "loaded",
"supports_options": False,
"supports_unload": False,
"title": "Test Entry",
},
"description": None,
"description_placeholders": None,
}

View File

@@ -2,6 +2,8 @@
from unittest.mock import patch
from pyownet.protocol import ProtocolError
from homeassistant.components.onewire.const import (
CONF_MOUNT_DIR,
CONF_NAMES,
@@ -13,6 +15,8 @@ from homeassistant.components.onewire.const import (
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from .const import MOCK_OWPROXY_DEVICES
from tests.common import MockConfigEntry
@@ -89,3 +93,35 @@ async def setup_onewire_patched_owserver_integration(hass):
await hass.async_block_till_done()
return config_entry
def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None:
"""Set up mock for owproxy."""
dir_return_value = []
main_read_side_effect = []
sub_read_side_effect = []
for device_id in device_ids:
mock_device = MOCK_OWPROXY_DEVICES[device_id]
# Setup directory listing
dir_return_value += [f"/{device_id}/"]
# Setup device reads
main_read_side_effect += [device_id[0:2].encode()]
if "inject_reads" in mock_device:
main_read_side_effect += mock_device["inject_reads"]
# Setup sub-device reads
device_sensors = mock_device.get(domain, [])
for expected_sensor in device_sensors:
sub_read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect = (
main_read_side_effect
+ sub_read_side_effect
+ [ProtocolError("Missing injected value")] * 20
)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect

View File

@@ -1,11 +1,10 @@
"""Tests for 1-Wire devices connected on OWServer."""
from unittest.mock import patch
"""Constants for 1-Wire integration."""
from pi1wire import InvalidCRCException, UnsupportResponseException
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.onewire.const import DOMAIN, PLATFORMS, PRESSURE_CBAR
from homeassistant.components.onewire.const import DOMAIN, PRESSURE_CBAR
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
@@ -24,13 +23,8 @@ from homeassistant.const import (
TEMP_CELSIUS,
VOLT,
)
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from tests.common import mock_device_registry, mock_registry
MOCK_DEVICE_SENSORS = {
MOCK_OWPROXY_DEVICES = {
"00.111111111111": {
"inject_reads": [
b"", # read device type
@@ -186,7 +180,42 @@ MOCK_DEVICE_SENSORS = {
"model": "DS2409",
"name": "1F.111111111111",
},
SENSOR_DOMAIN: [],
"branches": {
"aux": {},
"main": {
"1D.111111111111": {
"inject_reads": [
b"DS2423", # read device type
],
"device_info": {
"identifiers": {(DOMAIN, "1D.111111111111")},
"manufacturer": "Maxim Integrated",
"model": "DS2423",
"name": "1D.111111111111",
},
SENSOR_DOMAIN: [
{
"entity_id": "sensor.1d_111111111111_counter_a",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
"unique_id": "/1D.111111111111/counter.A",
"injected_value": b" 251123",
"result": "251123",
"unit": "count",
"class": None,
},
{
"entity_id": "sensor.1d_111111111111_counter_b",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
"unique_id": "/1D.111111111111/counter.B",
"injected_value": b" 248125",
"result": "248125",
"unit": "count",
"class": None,
},
],
},
},
},
},
"22.111111111111": {
"inject_reads": [
@@ -748,65 +777,106 @@ MOCK_DEVICE_SENSORS = {
},
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
@pytest.mark.parametrize("platform", PLATFORMS)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor.get(platform, [])
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 20)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
with patch("homeassistant.components.onewire.PLATFORMS", [platform]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
assert registry_entry.disabled == expected_sensor.get("disabled", False)
state = hass.states.get(entity_id)
if registry_entry.disabled:
assert state is None
else:
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
"device_file", registry_entry.unique_id
)
MOCK_SYSBUS_DEVICES = {
"00-111111111111": {"sensors": []},
"10-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "10-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "10",
"name": "10-111111111111",
},
"sensors": [
{
"entity_id": "sensor.my_ds18b20_temperature",
"unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
"injected_value": 25.123,
"result": "25.1",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"12-111111111111": {"sensors": []},
"1D-111111111111": {"sensors": []},
"22-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "22-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "22",
"name": "22-111111111111",
},
"sensors": [
{
"entity_id": "sensor.22_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
"injected_value": FileNotFoundError,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"26-111111111111": {"sensors": []},
"28-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "28-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "28",
"name": "28-111111111111",
},
"sensors": [
{
"entity_id": "sensor.28_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
"injected_value": InvalidCRCException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"29-111111111111": {"sensors": []},
"3B-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "3B-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "3B",
"name": "3B-111111111111",
},
"sensors": [
{
"entity_id": "sensor.3b_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
"injected_value": 29.993,
"result": "30.0",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"42-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "42-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "42",
"name": "42-111111111111",
},
"sensors": [
{
"entity_id": "sensor.42_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
"injected_value": UnsupportResponseException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"EF-111111111111": {
"sensors": [],
},
"EF-111111111112": {
"sensors": [],
},
}

View File

@@ -2,40 +2,25 @@
import copy
from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
MOCK_DEVICE_SENSORS = {
"12.111111111111": {
"inject_reads": [
b"DS2406", # read device type
],
BINARY_SENSOR_DOMAIN: [
{
"entity_id": "binary_sensor.12_111111111111_sensed_a",
"injected_value": b" 1",
"result": STATE_ON,
},
{
"entity_id": "binary_sensor.12_111111111111_sensed_b",
"injected_value": b" 0",
"result": STATE_OFF,
},
],
},
MOCK_BINARY_SENSORS = {
key: value
for (key, value) in MOCK_OWPROXY_DEVICES.items()
if BINARY_SENSOR_DOMAIN in value
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_binary_sensor(owproxy, hass, device_id):
"""Test for 1-Wire binary sensor.
@@ -45,26 +30,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id])
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
mock_device = MOCK_BINARY_SENSORS[device_id]
expected_entities = mock_device[BINARY_SENSOR_DOMAIN]
# Force enable binary sensors
patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS)
for item in patch_device_binary_sensors[device_family]:
for item in patch_device_binary_sensors[device_id[0:2]]:
item["default_disabled"] = False
with patch(
@@ -76,14 +49,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
assert len(entity_registry.entities) == len(expected_entities)
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)

View File

@@ -1,175 +0,0 @@
"""Tests for 1-Wire devices connected on SysBus."""
from unittest.mock import patch
from pi1wire import InvalidCRCException, UnsupportResponseException
import pytest
from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
from homeassistant.setup import async_setup_component
from tests.common import mock_device_registry, mock_registry
MOCK_CONFIG = {
SENSOR_DOMAIN: {
"platform": DOMAIN,
"mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
"names": {
"10-111111111111": "My DS18B20",
},
}
}
MOCK_DEVICE_SENSORS = {
"00-111111111111": {"sensors": []},
"10-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "10-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "10",
"name": "10-111111111111",
},
"sensors": [
{
"entity_id": "sensor.my_ds18b20_temperature",
"unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
"injected_value": 25.123,
"result": "25.1",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"12-111111111111": {"sensors": []},
"1D-111111111111": {"sensors": []},
"22-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "22-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "22",
"name": "22-111111111111",
},
"sensors": [
{
"entity_id": "sensor.22_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
"injected_value": FileNotFoundError,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"26-111111111111": {"sensors": []},
"28-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "28-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "28",
"name": "28-111111111111",
},
"sensors": [
{
"entity_id": "sensor.28_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
"injected_value": InvalidCRCException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"29-111111111111": {"sensors": []},
"3B-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "3B-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "3B",
"name": "3B-111111111111",
},
"sensors": [
{
"entity_id": "sensor.3b_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
"injected_value": 29.993,
"result": "30.0",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"42-111111111111": {
"device_info": {
"identifiers": {(DOMAIN, "42-111111111111")},
"manufacturer": "Maxim Integrated",
"model": "42",
"name": "42-111111111111",
},
"sensors": [
{
"entity_id": "sensor.42_111111111111_temperature",
"unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
"injected_value": UnsupportResponseException,
"result": "unknown",
"unit": TEMP_CELSIUS,
"class": DEVICE_CLASS_TEMPERATURE,
},
],
},
"EF-111111111111": {
"sensors": [],
},
"EF-111111111112": {
"sensors": [],
},
}
@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
async def test_onewiredirect_setup_valid_device(hass, device_id):
"""Test that sysbus config entry works correctly."""
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
read_side_effect = []
expected_sensors = mock_device_sensor["sensors"]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
"pi1wire.OneWire.get_temperature",
side_effect=read_side_effect,
):
assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
from pyownet.protocol import ConnError, OwnetError
from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import (
CONN_CLASS_LOCAL_POLL,
ENTRY_STATE_LOADED,
@@ -11,10 +12,17 @@ from homeassistant.config_entries import (
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration
from . import (
setup_onewire_owserver_integration,
setup_onewire_patched_owserver_integration,
setup_onewire_sysbus_integration,
setup_owproxy_mock_devices,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
async def test_owserver_connect_failure(hass):
@@ -87,3 +95,41 @@ async def test_unload_entry(hass):
assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED
assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_registry_cleanup(owproxy, hass):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
# Initialise with two components
setup_owproxy_mock_devices(
owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"]
)
with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2
# Second item has disappeared from bus, and was removed manually from the front-end
setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"])
entity_registry.async_remove("sensor.28_111111111111_temperature")
await hass.async_block_till_done()
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
# Second item has disappeared from bus, and was removed manually from the front-end
with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
await hass.config_entries.async_reload("2")
await hass.async_block_till_done()
assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1

View File

@@ -4,54 +4,29 @@ from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
from homeassistant.components.onewire.const import (
DEFAULT_SYSBUS_MOUNT_DIR,
DOMAIN,
PLATFORMS,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES
from tests.common import assert_setup_component, mock_registry
from tests.common import assert_setup_component, mock_device_registry, mock_registry
MOCK_COUPLERS = {
"1F.111111111111": {
"inject_reads": [
b"DS2409", # read device type
],
"branches": {
"aux": {},
"main": {
"1D.111111111111": {
"inject_reads": [
b"DS2423", # read device type
],
"device_info": {
"identifiers": {(DOMAIN, "1D.111111111111")},
"manufacturer": "Maxim Integrated",
"model": "DS2423",
"name": "1D.111111111111",
},
SENSOR_DOMAIN: [
{
"entity_id": "sensor.1d_111111111111_counter_a",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
"unique_id": "/1D.111111111111/counter.A",
"injected_value": b" 251123",
"result": "251123",
"unit": "count",
"class": None,
},
{
"entity_id": "sensor.1d_111111111111_counter_b",
"device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
"unique_id": "/1D.111111111111/counter.B",
"injected_value": b" 248125",
"result": "248125",
"unit": "count",
"class": None,
},
],
},
},
key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value
}
MOCK_SYSBUS_CONFIG = {
SENSOR_DOMAIN: {
"platform": DOMAIN,
"mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
"names": {
"10-111111111111": "My DS18B20",
},
}
}
@@ -154,3 +129,103 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id):
else:
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor["device_file"]
@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys())
@pytest.mark.parametrize("platform", PLATFORMS)
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
"""Test for 1-Wire device.
As they would be on a clean setup: all binary-sensors and switches disabled.
"""
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
setup_owproxy_mock_devices(owproxy, platform, [device_id])
mock_device = MOCK_OWPROXY_DEVICES[device_id]
expected_entities = mock_device.get(platform, [])
with patch("homeassistant.components.onewire.PLATFORMS", [platform]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_entities)
if len(expected_entities) > 0:
device_info = mock_device["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_entity["unique_id"]
assert registry_entry.unit_of_measurement == expected_entity["unit"]
assert registry_entry.device_class == expected_entity["class"]
assert registry_entry.disabled == expected_entity.get("disabled", False)
state = hass.states.get(entity_id)
if registry_entry.disabled:
assert state is None
else:
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)
@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys())
async def test_onewiredirect_setup_valid_device(hass, device_id):
"""Test that sysbus config entry works correctly."""
entity_registry = mock_registry(hass)
device_registry = mock_device_registry(hass)
mock_device_sensor = MOCK_SYSBUS_DEVICES[device_id]
glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
read_side_effect = []
expected_sensors = mock_device_sensor["sensors"]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
"pi1wire.OneWire.get_temperature",
side_effect=read_side_effect,
):
assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
if len(expected_sensors) > 0:
device_info = mock_device_sensor["device_info"]
assert len(device_registry.devices) == 1
registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
assert registry_entry is not None
assert registry_entry.identifiers == {(DOMAIN, device_id)}
assert registry_entry.manufacturer == device_info["manufacturer"]
assert registry_entry.name == device_info["name"]
assert registry_entry.model == device_info["model"]
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
assert registry_entry.unique_id == expected_sensor["unique_id"]
assert registry_entry.unit_of_measurement == expected_sensor["unit"]
assert registry_entry.device_class == expected_sensor["class"]
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]

View File

@@ -2,7 +2,6 @@
import copy
from unittest.mock import patch
from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.onewire.switch import DEVICE_SWITCHES
@@ -10,58 +9,19 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
from . import setup_onewire_patched_owserver_integration
from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
MOCK_DEVICE_SENSORS = {
"12.111111111111": {
"inject_reads": [
b"DS2406", # read device type
],
SWITCH_DOMAIN: [
{
"entity_id": "switch.12_111111111111_pio_a",
"unique_id": "/12.111111111111/PIO.A",
"injected_value": b" 1",
"result": STATE_ON,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_pio_b",
"unique_id": "/12.111111111111/PIO.B",
"injected_value": b" 0",
"result": STATE_OFF,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_latch_a",
"unique_id": "/12.111111111111/latch.A",
"injected_value": b" 1",
"result": STATE_ON,
"unit": None,
"class": None,
"disabled": True,
},
{
"entity_id": "switch.12_111111111111_latch_b",
"unique_id": "/12.111111111111/latch.B",
"injected_value": b" 0",
"result": STATE_OFF,
"unit": None,
"class": None,
"disabled": True,
},
],
}
MOCK_SWITCHES = {
key: value
for (key, value) in MOCK_OWPROXY_DEVICES.items()
if SWITCH_DOMAIN in value
}
@pytest.mark.parametrize("device_id", ["12.111111111111"])
@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_switch(owproxy, hass, device_id):
"""Test for 1-Wire switch.
@@ -71,26 +31,14 @@ async def test_owserver_switch(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id])
device_family = device_id[0:2]
dir_return_value = [f"/{device_id}/"]
read_side_effect = [device_family.encode()]
if "inject_reads" in mock_device_sensor:
read_side_effect += mock_device_sensor["inject_reads"]
expected_sensors = mock_device_sensor[SWITCH_DOMAIN]
for expected_sensor in expected_sensors:
read_side_effect.append(expected_sensor["injected_value"])
# Ensure enough read side effect
read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
owproxy.return_value.dir.return_value = dir_return_value
owproxy.return_value.read.side_effect = read_side_effect
mock_device = MOCK_SWITCHES[device_id]
expected_entities = mock_device[SWITCH_DOMAIN]
# Force enable switches
patch_device_switches = copy.deepcopy(DEVICE_SWITCHES)
for item in patch_device_switches[device_family]:
for item in patch_device_switches[device_id[0:2]]:
item["default_disabled"] = False
with patch(
@@ -101,21 +49,21 @@ async def test_owserver_switch(owproxy, hass, device_id):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
assert len(entity_registry.entities) == len(expected_sensors)
assert len(entity_registry.entities) == len(expected_entities)
for expected_sensor in expected_sensors:
entity_id = expected_sensor["entity_id"]
for expected_entity in expected_entities:
entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.state == expected_entity["result"]
if state.state == STATE_ON:
owproxy.return_value.read.side_effect = [b" 0"]
expected_sensor["result"] = STATE_OFF
expected_entity["result"] = STATE_OFF
elif state.state == STATE_OFF:
owproxy.return_value.read.side_effect = [b" 1"]
expected_sensor["result"] = STATE_ON
expected_entity["result"] = STATE_ON
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -126,7 +74,7 @@ async def test_owserver_switch(owproxy, hass, device_id):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor.get(
assert state.state == expected_entity["result"]
assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)

View File

@@ -1,9 +1,11 @@
"""Test Trace websocket API."""
import asyncio
import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.trace.const import STORED_TRACES
from homeassistant.core import Context
from homeassistant.core import Context, callback
from homeassistant.helpers.typing import UNDEFINED
from tests.common import assert_lists_same
@@ -167,9 +169,11 @@ async def test_get_trace(
assert trace["trace"][f"{prefix}/0"][0]["error"]
assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action
_assert_raw_config(domain, sun_config, trace)
assert trace["blueprint_inputs"] is None
assert trace["context"]
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["script_execution"] == "error"
assert trace["item_id"] == "sun"
assert trace["context"][context_key] == context.id
assert trace.get("trigger", UNDEFINED) == trigger[0]
@@ -207,9 +211,11 @@ async def test_get_trace(
assert "error" not in trace["trace"][f"{prefix}/0"][0]
assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action
_assert_raw_config(domain, moon_config, trace)
assert trace["blueprint_inputs"] is None
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == "finished"
assert trace["item_id"] == "moon"
assert trace.get("trigger", UNDEFINED) == trigger[1]
@@ -260,6 +266,7 @@ async def test_get_trace(
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == "failed_conditions"
assert trace["trigger"] == "event 'test_event3'"
assert trace["item_id"] == "moon"
contexts[trace["context"]["id"]] = {
@@ -301,6 +308,7 @@ async def test_get_trace(
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == "finished"
assert trace["trigger"] == "event 'test_event2'"
assert trace["item_id"] == "moon"
contexts[trace["context"]["id"]] = {
@@ -391,7 +399,7 @@ async def test_trace_overflow(hass, hass_ws_client, domain):
@pytest.mark.parametrize(
"domain, prefix, trigger, last_step",
"domain, prefix, trigger, last_step, script_execution",
[
(
"automation",
@@ -403,16 +411,20 @@ async def test_trace_overflow(hass, hass_ws_client, domain):
"event 'test_event2'",
],
["{prefix}/0", "{prefix}/0", "condition/0", "{prefix}/0"],
["error", "finished", "failed_conditions", "finished"],
),
(
"script",
"sequence",
[UNDEFINED, UNDEFINED, UNDEFINED, UNDEFINED],
["{prefix}/0", "{prefix}/0", "{prefix}/0", "{prefix}/0"],
["error", "finished", "finished", "finished"],
),
],
)
async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_step):
async def test_list_traces(
hass, hass_ws_client, domain, prefix, trigger, last_step, script_execution
):
"""Test listing script and automation traces."""
id = 1
@@ -458,7 +470,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
await _run_automation_or_script(hass, domain, sun_config, "test_event")
await hass.async_block_till_done()
# Get trace
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
response = await client.receive_json()
assert response["success"]
@@ -492,7 +504,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
await _run_automation_or_script(hass, domain, moon_config, "test_event2")
await hass.async_block_till_done()
# Get trace
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
response = await client.receive_json()
assert response["success"]
@@ -502,6 +514,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
assert trace["last_step"] == last_step[0].format(prefix=prefix)
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["script_execution"] == script_execution[0]
assert trace["timestamp"]
assert trace["item_id"] == "sun"
assert trace.get("trigger", UNDEFINED) == trigger[0]
@@ -510,6 +523,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
assert trace["last_step"] == last_step[1].format(prefix=prefix)
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == script_execution[1]
assert trace["timestamp"]
assert trace["item_id"] == "moon"
assert trace.get("trigger", UNDEFINED) == trigger[1]
@@ -518,6 +532,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
assert trace["last_step"] == last_step[2].format(prefix=prefix)
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == script_execution[2]
assert trace["timestamp"]
assert trace["item_id"] == "moon"
assert trace.get("trigger", UNDEFINED) == trigger[2]
@@ -526,6 +541,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s
assert trace["last_step"] == last_step[3].format(prefix=prefix)
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["script_execution"] == script_execution[3]
assert trace["timestamp"]
assert trace["item_id"] == "moon"
assert trace.get("trigger", UNDEFINED) == trigger[3]
@@ -1006,3 +1022,213 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
"node": f"{prefix}/5",
"run_id": run_id,
}
@pytest.mark.parametrize(
"script_mode,max_runs,script_execution",
[
({"mode": "single"}, 1, "failed_single"),
({"mode": "parallel", "max": 2}, 2, "failed_max_runs"),
],
)
async def test_script_mode(
hass, hass_ws_client, script_mode, max_runs, script_execution
):
"""Test overlapping runs with max_runs > 1."""
id = 1
def next_id():
nonlocal id
id += 1
return id
flag = asyncio.Event()
@callback
def _handle_event(_):
flag.set()
event = "test_event"
script_config = {
"script1": {
"sequence": [
{"event": event, "event_data": {"value": 1}},
{"wait_template": "{{ states.switch.test.state == 'off' }}"},
{"event": event, "event_data": {"value": 2}},
],
**script_mode,
},
}
client = await hass_ws_client()
hass.bus.async_listen(event, _handle_event)
assert await async_setup_component(hass, "script", {"script": script_config})
for _ in range(max_runs):
hass.states.async_set("switch.test", "on")
await hass.services.async_call("script", "script1")
await asyncio.wait_for(flag.wait(), 1)
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
response = await client.receive_json()
assert response["success"]
traces = _find_traces(response["result"], "script", "script1")
assert len(traces) == max_runs
for trace in traces:
assert trace["state"] == "running"
# Start additional run of script while first runs are suspended in wait_template.
flag.clear()
await hass.services.async_call("script", "script1")
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
response = await client.receive_json()
assert response["success"]
traces = _find_traces(response["result"], "script", "script1")
assert len(traces) == max_runs + 1
assert traces[-1]["state"] == "stopped"
assert traces[-1]["script_execution"] == script_execution
@pytest.mark.parametrize(
"script_mode,script_execution",
[("restart", "cancelled"), ("parallel", "finished")],
)
async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution):
"""Test overlapping runs with max_runs > 1."""
id = 1
def next_id():
nonlocal id
id += 1
return id
flag = asyncio.Event()
@callback
def _handle_event(_):
flag.set()
event = "test_event"
script_config = {
"script1": {
"sequence": [
{"event": event, "event_data": {"value": 1}},
{"wait_template": "{{ states.switch.test.state == 'off' }}"},
{"event": event, "event_data": {"value": 2}},
],
"mode": script_mode,
}
}
client = await hass_ws_client()
hass.bus.async_listen(event, _handle_event)
assert await async_setup_component(hass, "script", {"script": script_config})
hass.states.async_set("switch.test", "on")
await hass.services.async_call("script", "script1")
await asyncio.wait_for(flag.wait(), 1)
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], "script", "script1")[0]
assert trace["state"] == "running"
# Start second run of script while first run is suspended in wait_template.
flag.clear()
await hass.services.async_call("script", "script1")
await asyncio.wait_for(flag.wait(), 1)
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], "script", "script1")[1]
assert trace["state"] == "running"
# Let both scripts finish
hass.states.async_set("switch.test", "off")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], "script", "script1")[0]
assert trace["state"] == "stopped"
assert trace["script_execution"] == script_execution
trace = _find_traces(response["result"], "script", "script1")[1]
assert trace["state"] == "stopped"
assert trace["script_execution"] == "finished"
async def test_trace_blueprint_automation(hass, hass_ws_client):
"""Test trace of blueprint automation."""
id = 1
def next_id():
nonlocal id
id += 1
return id
domain = "automation"
sun_config = {
"id": "sun",
"use_blueprint": {
"path": "test_event_service.yaml",
"input": {
"trigger_event": "blueprint_event",
"service_to_call": "test.automation",
},
},
}
sun_action = {
"limit": 10,
"params": {
"domain": "test",
"service": "automation",
"service_data": {},
"target": {"entity_id": ["light.kitchen"]},
},
"running_script": False,
}
assert await async_setup_component(hass, "automation", {"automation": sun_config})
client = await hass_ws_client()
hass.bus.async_fire("blueprint_event")
await hass.async_block_till_done()
# List traces
await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
response = await client.receive_json()
assert response["success"]
run_id = _find_run_id(response["result"], domain, "sun")
# Get trace
await client.send_json(
{
"id": next_id(),
"type": "trace/get",
"domain": domain,
"item_id": "sun",
"run_id": run_id,
}
)
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert set(trace["trace"]) == {"trigger/0", "action/0"}
assert len(trace["trace"]["action/0"]) == 1
assert trace["trace"]["action/0"][0]["error"]
assert trace["trace"]["action/0"][0]["result"] == sun_action
assert trace["config"]["id"] == "sun"
assert trace["blueprint_inputs"] == sun_config
assert trace["context"]
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["script_execution"] == "error"
assert trace["item_id"] == "sun"
assert trace.get("trigger", UNDEFINED) == "event 'blueprint_event'"

View File

@@ -86,9 +86,10 @@ def assert_element(trace_element, expected_element, path):
assert not trace_element._variables
def assert_action_trace(expected):
def assert_action_trace(expected, expected_script_execution="finished"):
"""Assert a trace condition sequence is as expected."""
action_trace = trace.trace_get(clear=False)
script_execution = trace.script_execution_get()
trace.trace_clear()
expected_trace_keys = list(expected.keys())
assert list(action_trace.keys()) == expected_trace_keys
@@ -98,6 +99,8 @@ def assert_action_trace(expected):
path = f"[{trace_key_index}][{index}]"
assert_element(action_trace[key][index], element, path)
assert script_execution == expected_script_execution
def async_watch_for_action(script_obj, message):
"""Watch for message in last_action."""
@@ -620,7 +623,8 @@ async def test_delay_template_invalid(hass, caplog):
{
"0": [{"result": {"event": "test_event", "event_data": {}}}],
"1": [{"error_type": script._StopScript}],
}
},
expected_script_execution="aborted",
)
@@ -680,7 +684,8 @@ async def test_delay_template_complex_invalid(hass, caplog):
{
"0": [{"result": {"event": "test_event", "event_data": {}}}],
"1": [{"error_type": script._StopScript}],
}
},
expected_script_execution="aborted",
)
@@ -717,7 +722,8 @@ async def test_cancel_delay(hass):
assert_action_trace(
{
"0": [{"result": {"delay": 5.0, "done": False}}],
}
},
expected_script_execution="cancelled",
)
@@ -969,13 +975,15 @@ async def test_cancel_wait(hass, action_type):
assert_action_trace(
{
"0": [{"result": {"wait": {"completed": False, "remaining": None}}}],
}
},
expected_script_execution="cancelled",
)
else:
assert_action_trace(
{
"0": [{"result": {"wait": {"trigger": None, "remaining": None}}}],
}
},
expected_script_execution="cancelled",
)
@@ -1131,6 +1139,7 @@ async def test_wait_continue_on_timeout(
if continue_on_timeout is False:
expected_trace["0"][0]["result"]["timeout"] = True
expected_trace["0"][0]["error_type"] = script._StopScript
expected_script_execution = "aborted"
else:
expected_trace["1"] = [
{
@@ -1138,7 +1147,8 @@ async def test_wait_continue_on_timeout(
"variables": variable_wait,
}
]
assert_action_trace(expected_trace)
expected_script_execution = "finished"
assert_action_trace(expected_trace, expected_script_execution)
async def test_wait_template_variables_in(hass):
@@ -1404,7 +1414,8 @@ async def test_condition_warning(hass, caplog):
"1": [{"error_type": script._StopScript, "result": {"result": False}}],
"1/condition": [{"error_type": ConditionError}],
"1/condition/entity_id/0": [{"error_type": ConditionError}],
}
},
expected_script_execution="aborted",
)
@@ -1456,7 +1467,8 @@ async def test_condition_basic(hass, caplog):
"0": [{"result": {"event": "test_event", "event_data": {}}}],
"1": [{"error_type": script._StopScript, "result": {"result": False}}],
"1/condition": [{"result": {"result": False}}],
}
},
expected_script_execution="aborted",
)
@@ -2141,7 +2153,7 @@ async def test_propagate_error_service_not_found(hass):
}
],
}
assert_action_trace(expected_trace)
assert_action_trace(expected_trace, expected_script_execution="error")
async def test_propagate_error_invalid_service_data(hass):
@@ -2178,7 +2190,7 @@ async def test_propagate_error_invalid_service_data(hass):
}
],
}
assert_action_trace(expected_trace)
assert_action_trace(expected_trace, expected_script_execution="error")
async def test_propagate_error_service_exception(hass):
@@ -2219,7 +2231,7 @@ async def test_propagate_error_service_exception(hass):
}
],
}
assert_action_trace(expected_trace)
assert_action_trace(expected_trace, expected_script_execution="error")
async def test_referenced_entities(hass):

View File

@@ -213,6 +213,30 @@ class TestServiceHelpers(unittest.TestCase):
"entity_id": ["light.static", "light.dynamic"],
}
config = {
"service": "{{ 'test_domain.test_service' }}",
"target": "{{ var_target }}",
}
service.call_from_config(
self.hass,
config,
variables={
"var_target": {
"entity_id": "light.static",
"area_id": ["area-42", "area-51"],
},
},
)
service.call_from_config(self.hass, config)
self.hass.block_till_done()
assert dict(self.calls[2].data) == {
"area_id": ["area-42", "area-51"],
"entity_id": ["light.static"],
}
def test_service_template_service_call(self):
"""Test legacy service_template call with templating."""
config = {

View File

@@ -1,11 +1,13 @@
"""Test Home Assistant logging util methods."""
import asyncio
from functools import partial
import logging
import queue
from unittest.mock import patch
import pytest
from homeassistant.core import callback, is_callback
import homeassistant.util.logging as logging_util
@@ -80,3 +82,30 @@ async def test_async_create_catching_coro(hass, caplog):
await hass.async_block_till_done()
assert "This is a bad coroutine" in caplog.text
assert "in test_async_create_catching_coro" in caplog.text
def test_catch_log_exception():
"""Test it is still a callback after wrapping including partial."""
async def async_meth():
pass
assert asyncio.iscoroutinefunction(
logging_util.catch_log_exception(partial(async_meth), lambda: None)
)
@callback
def callback_meth():
pass
assert is_callback(
logging_util.catch_log_exception(partial(callback_meth), lambda: None)
)
def sync_meth():
pass
wrapped = logging_util.catch_log_exception(partial(sync_meth), lambda: None)
assert not is_callback(wrapped)
assert not asyncio.iscoroutinefunction(wrapped)