mirror of
https://github.com/home-assistant/core.git
synced 2026-04-12 20:56:18 +02:00
Compare commits
67 Commits
2021.4.0b0
...
2021.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c3bdad7d | ||
|
|
3bf693e352 | ||
|
|
7051cc04bd | ||
|
|
d9c1c391bc | ||
|
|
02cd2619bb | ||
|
|
f791142c75 | ||
|
|
1c939fc9be | ||
|
|
0ad4736349 | ||
|
|
3f0d63c1ab | ||
|
|
f39afa60ae | ||
|
|
cf11d9a2df | ||
|
|
dd2a73b363 | ||
|
|
99ef870908 | ||
|
|
8d738cff41 | ||
|
|
8bdcdfb8e6 | ||
|
|
341531146d | ||
|
|
49178d6865 | ||
|
|
b4636f17fb | ||
|
|
0fb4f31bde | ||
|
|
b382de96c6 | ||
|
|
c9f8861303 | ||
|
|
32511409a9 | ||
|
|
e366961ddb | ||
|
|
bfb8141f55 | ||
|
|
537d6412dd | ||
|
|
a093cd8ac2 | ||
|
|
322458ee49 | ||
|
|
b573fb49b7 | ||
|
|
15e00b8d18 | ||
|
|
2db60a3c56 | ||
|
|
ed90e22421 | ||
|
|
d61780dbac | ||
|
|
315e910bfe | ||
|
|
a7523777ba | ||
|
|
7ae65832eb | ||
|
|
0df9a8ec38 | ||
|
|
5f2a666e76 | ||
|
|
26b9017905 | ||
|
|
bdd68cd413 | ||
|
|
c512ab7ec9 | ||
|
|
edf41e8425 | ||
|
|
1850b92b36 | ||
|
|
7b1ea46653 | ||
|
|
a8cd6228cf | ||
|
|
9eb4397837 | ||
|
|
311f624adc | ||
|
|
dcb43b474f | ||
|
|
396a8a3a10 | ||
|
|
2a1f6d7e8f | ||
|
|
da31328150 | ||
|
|
cec80210a3 | ||
|
|
74357d9760 | ||
|
|
231a55d416 | ||
|
|
e760c23f37 | ||
|
|
39f68de5fa | ||
|
|
68b189cf9f | ||
|
|
8d0941ba65 | ||
|
|
d1a48c7c5c | ||
|
|
f0f8b79be0 | ||
|
|
c2d17a72b7 | ||
|
|
f0bd3c577f | ||
|
|
947ac514b9 | ||
|
|
5df90b32fc | ||
|
|
f08e7dccdf | ||
|
|
3982849275 | ||
|
|
07827ca55d | ||
|
|
16da181692 |
@@ -283,7 +283,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
temperature_feeling = None
|
||||
town_id = None
|
||||
town_name = None
|
||||
town_timestamp = dt_util.as_utc(elaborated)
|
||||
town_timestamp = dt_util.as_utc(elaborated).isoformat()
|
||||
wind_bearing = None
|
||||
wind_max_speed = None
|
||||
wind_speed = None
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
|
||||
from .analytics import Analytics
|
||||
from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||
from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, _):
|
||||
@@ -44,10 +44,9 @@ async def websocket_analytics(
|
||||
) -> None:
|
||||
"""Return analytics preferences."""
|
||||
analytics: Analytics = hass.data[DOMAIN]
|
||||
huuid = await hass.helpers.instance_id.async_get()
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid},
|
||||
{ATTR_PREFERENCES: analytics.preferences},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Analytics helper class for the analytics integration."""
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@@ -7,7 +8,7 @@ import async_timeout
|
||||
from homeassistant.components import hassio
|
||||
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
|
||||
from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -22,9 +23,9 @@ from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_AUTOMATION_COUNT,
|
||||
ATTR_BASE,
|
||||
ATTR_CUSTOM_INTEGRATIONS,
|
||||
ATTR_DIAGNOSTICS,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_HUUID,
|
||||
ATTR_INTEGRATION_COUNT,
|
||||
ATTR_INTEGRATIONS,
|
||||
ATTR_ONBOARDED,
|
||||
@@ -37,6 +38,7 @@ from .const import (
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_USAGE,
|
||||
ATTR_USER_COUNT,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
LOGGER,
|
||||
PREFERENCE_SCHEMA,
|
||||
@@ -52,7 +54,7 @@ class Analytics:
|
||||
"""Initialize the Analytics class."""
|
||||
self.hass: HomeAssistant = hass
|
||||
self.session = async_get_clientsession(hass)
|
||||
self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False}
|
||||
self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None}
|
||||
self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@property
|
||||
@@ -71,6 +73,11 @@ class Analytics:
|
||||
"""Return bool if the user has made a choice."""
|
||||
return self._data[ATTR_ONBOARDED]
|
||||
|
||||
@property
|
||||
def uuid(self) -> bool:
|
||||
"""Return the uuid for the analytics integration."""
|
||||
return self._data[ATTR_UUID]
|
||||
|
||||
@property
|
||||
def supervisor(self) -> bool:
|
||||
"""Return bool if a supervisor is present."""
|
||||
@@ -81,6 +88,7 @@ class Analytics:
|
||||
stored = await self._store.async_load()
|
||||
if stored:
|
||||
self._data = stored
|
||||
|
||||
if self.supervisor:
|
||||
supervisor_info = hassio.get_supervisor_info(self.hass)
|
||||
if not self.onboarded:
|
||||
@@ -99,6 +107,7 @@ class Analytics:
|
||||
preferences = PREFERENCE_SCHEMA(preferences)
|
||||
self._data[ATTR_PREFERENCES].update(preferences)
|
||||
self._data[ATTR_ONBOARDED] = True
|
||||
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
if self.supervisor:
|
||||
@@ -114,16 +123,19 @@ class Analytics:
|
||||
LOGGER.debug("Nothing to submit")
|
||||
return
|
||||
|
||||
huuid = await self.hass.helpers.instance_id.async_get()
|
||||
if self._data.get(ATTR_UUID) is None:
|
||||
self._data[ATTR_UUID] = uuid.uuid4().hex
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
if self.supervisor:
|
||||
supervisor_info = hassio.get_supervisor_info(self.hass)
|
||||
|
||||
system_info = await async_get_system_info(self.hass)
|
||||
integrations = []
|
||||
custom_integrations = []
|
||||
addons = []
|
||||
payload: dict = {
|
||||
ATTR_HUUID: huuid,
|
||||
ATTR_UUID: self.uuid,
|
||||
ATTR_VERSION: HA_VERSION,
|
||||
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
|
||||
}
|
||||
@@ -152,7 +164,16 @@ class Analytics:
|
||||
if isinstance(integration, BaseException):
|
||||
raise integration
|
||||
|
||||
if integration.disabled or not integration.is_built_in:
|
||||
if integration.disabled:
|
||||
continue
|
||||
|
||||
if not integration.is_built_in:
|
||||
custom_integrations.append(
|
||||
{
|
||||
ATTR_DOMAIN: integration.domain,
|
||||
ATTR_VERSION: integration.version,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
integrations.append(integration.domain)
|
||||
@@ -176,6 +197,7 @@ class Analytics:
|
||||
|
||||
if self.preferences.get(ATTR_USAGE, False):
|
||||
payload[ATTR_INTEGRATIONS] = integrations
|
||||
payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations
|
||||
if supervisor_info is not None:
|
||||
payload[ATTR_ADDONS] = addons
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ ATTR_ADDONS = "addons"
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_AUTOMATION_COUNT = "automation_count"
|
||||
ATTR_BASE = "base"
|
||||
ATTR_CUSTOM_INTEGRATIONS = "custom_integrations"
|
||||
ATTR_DIAGNOSTICS = "diagnostics"
|
||||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_HUUID = "huuid"
|
||||
ATTR_INSTALLATION_TYPE = "installation_type"
|
||||
ATTR_INTEGRATION_COUNT = "integration_count"
|
||||
ATTR_INTEGRATIONS = "integrations"
|
||||
@@ -34,6 +34,7 @@ ATTR_SUPERVISOR = "supervisor"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_USAGE = "usage"
|
||||
ATTR_USER_COUNT = "user_count"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_VERSION = "version"
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*" },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Google Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"requirements": ["pychromecast==9.1.1"],
|
||||
"requirements": ["pychromecast==9.1.2"],
|
||||
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
|
||||
"zeroconf": ["_googlecast._tcp.local."],
|
||||
"codeowners": ["@emontnemery"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Provide functionality to interact with Cast devices on the network."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
@@ -185,7 +186,9 @@ class CastDevice(MediaPlayerEntity):
|
||||
)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
|
||||
self.async_set_cast_info(self._cast_info)
|
||||
self.hass.async_create_task(
|
||||
# asyncio.create_task is used to avoid delaying startup wrapup if the device
|
||||
# is discovered already during startup but then fails to respond
|
||||
asyncio.create_task(
|
||||
async_create_catching_coro(self.async_connect_to_chromecast())
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "Default Config",
|
||||
"documentation": "https://www.home-assistant.io/integrations/default_config",
|
||||
"dependencies": [
|
||||
"analytics",
|
||||
"automation",
|
||||
"cloud",
|
||||
"counter",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "DHCP Discovery",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
||||
"requirements": [
|
||||
"scapy==2.4.4", "aiodiscover==1.3.2"
|
||||
"scapy==2.4.4", "aiodiscover==1.3.3"
|
||||
],
|
||||
"codeowners": [
|
||||
"@bdraco"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Support for DoorBird devices."""
|
||||
import asyncio
|
||||
import logging
|
||||
import urllib
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from aiohttp import web
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
@@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
device = DoorBird(device_ip, username, password)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_init_doorbird_device, device)
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == HTTP_UNAUTHORIZED:
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
_LOGGER.error(
|
||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
||||
)
|
||||
@@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
async def _async_register_events(hass, doorstation):
|
||||
try:
|
||||
await hass.async_add_executor_job(doorstation.register_events, hass)
|
||||
except HTTPError:
|
||||
except requests.exceptions.HTTPError:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Doorbird configuration failed. Please verify that API "
|
||||
"Operator permission is enabled for the Doorbird user. "
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Config flow for DoorBird integration."""
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
@@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None):
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
def _check_device(device):
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||
try:
|
||||
status = await hass.async_add_executor_job(device.ready)
|
||||
info = await hass.async_add_executor_job(device.info)
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == HTTP_UNAUTHORIZED:
|
||||
status, info = await hass.async_add_executor_job(_check_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
raise InvalidAuth from err
|
||||
raise CannotConnect from err
|
||||
except OSError as err:
|
||||
@@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
return {"title": data[CONF_HOST], "mac_addr": mac_addr}
|
||||
|
||||
|
||||
async def async_verify_supported_device(hass, host):
|
||||
"""Verify the doorbell state endpoint returns a 401."""
|
||||
device = DoorBird(host, "", "")
|
||||
try:
|
||||
await hass.async_add_executor_job(device.doorbell_state)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for DoorBird."""
|
||||
|
||||
@@ -85,17 +99,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered doorbird device."""
|
||||
macaddress = discovery_info["properties"]["macaddress"]
|
||||
host = discovery_info[CONF_HOST]
|
||||
|
||||
if macaddress[:6] != DOORBIRD_OUI:
|
||||
return self.async_abort(reason="not_doorbird_device")
|
||||
if is_link_local(ip_address(discovery_info[CONF_HOST])):
|
||||
if is_link_local(ip_address(host)):
|
||||
return self.async_abort(reason="link_local_address")
|
||||
if not await async_verify_supported_device(self.hass, host):
|
||||
return self.async_abort(reason="not_doorbird_device")
|
||||
|
||||
await self.async_set_unique_id(macaddress)
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: discovery_info[CONF_HOST]}
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
chop_ending = "._axis-video._tcp.local."
|
||||
friendly_hostname = discovery_info["name"]
|
||||
@@ -104,11 +119,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
CONF_NAME: friendly_hostname,
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_HOST: host,
|
||||
}
|
||||
self.discovery_schema = _schema_with_defaults(
|
||||
host=discovery_info[CONF_HOST], name=friendly_hostname
|
||||
)
|
||||
self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
# Flag to check if the device is connected
|
||||
self._connected = True
|
||||
self._connected_lock = asyncio.Lock()
|
||||
self._zc_lock = asyncio.Lock()
|
||||
self._zc_listening = False
|
||||
# Event the different strategies use for issuing a reconnect attempt.
|
||||
self._reconnect_event = asyncio.Event()
|
||||
# The task containing the infinite reconnect loop while running
|
||||
@@ -270,6 +272,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._entry_data.disconnect_callbacks = []
|
||||
self._entry_data.available = False
|
||||
self._entry_data.async_update_device_state(self._hass)
|
||||
await self._start_zc_listen()
|
||||
|
||||
# Reset tries
|
||||
async with self._tries_lock:
|
||||
@@ -315,6 +318,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._host,
|
||||
error,
|
||||
)
|
||||
await self._start_zc_listen()
|
||||
# Schedule re-connect in event loop in order not to delay HA
|
||||
# startup. First connect is scheduled in tracked tasks.
|
||||
async with self._wait_task_lock:
|
||||
@@ -332,6 +336,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._tries = 0
|
||||
async with self._connected_lock:
|
||||
self._connected = True
|
||||
await self._stop_zc_listen()
|
||||
self._hass.async_create_task(self._on_login())
|
||||
|
||||
async def _reconnect_once(self):
|
||||
@@ -375,9 +380,6 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
# Create reconnection loop outside of HA's tracked tasks in order
|
||||
# not to delay startup.
|
||||
self._loop_task = self._hass.loop.create_task(self._reconnect_loop())
|
||||
# Listen for mDNS records so we can reconnect directly if a received mDNS record
|
||||
# indicates the node is up again
|
||||
await self._hass.async_add_executor_job(self._zc.add_listener, self, None)
|
||||
|
||||
async with self._connected_lock:
|
||||
self._connected = False
|
||||
@@ -388,11 +390,31 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
if self._loop_task is not None:
|
||||
self._loop_task.cancel()
|
||||
self._loop_task = None
|
||||
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
|
||||
async with self._wait_task_lock:
|
||||
if self._wait_task is not None:
|
||||
self._wait_task.cancel()
|
||||
self._wait_task = None
|
||||
await self._stop_zc_listen()
|
||||
|
||||
async def _start_zc_listen(self):
|
||||
"""Listen for mDNS records.
|
||||
|
||||
This listener allows us to schedule a reconnect as soon as a
|
||||
received mDNS record indicates the node is up again.
|
||||
"""
|
||||
async with self._zc_lock:
|
||||
if not self._zc_listening:
|
||||
await self._hass.async_add_executor_job(
|
||||
self._zc.add_listener, self, None
|
||||
)
|
||||
self._zc_listening = True
|
||||
|
||||
async def _stop_zc_listen(self):
|
||||
"""Stop listening for zeroconf updates."""
|
||||
async with self._zc_lock:
|
||||
if self._zc_listening:
|
||||
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
|
||||
self._zc_listening = False
|
||||
|
||||
@callback
|
||||
def stop_callback(self):
|
||||
|
||||
@@ -62,7 +62,7 @@ MANIFEST_JSON = {
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/images/screenshots/screenshot-1.png",
|
||||
"sizes": "413×792",
|
||||
"sizes": "413x792",
|
||||
"type": "image/png",
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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==20210407.2"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Adds support for generic thermostat units."""
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -419,7 +420,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
try:
|
||||
self._cur_temp = float(state.state)
|
||||
cur_temp = float(state.state)
|
||||
if math.isnan(cur_temp) or math.isinf(cur_temp):
|
||||
raise ValueError(f"Sensor has illegal state {state.state}")
|
||||
self._cur_temp = cur_temp
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update from sensor: %s", ex)
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# We only need one Hass.io config entry
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=DOMAIN.title(), data={})
|
||||
return self.async_create_entry(title="Supervisor", data={})
|
||||
|
||||
@@ -715,7 +715,7 @@ class LazyState(State):
|
||||
self._attributes = json.loads(self._row.attributes)
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to state: %s", self)
|
||||
_LOGGER.exception("Error converting row to state: %s", self._row)
|
||||
self._attributes = {}
|
||||
return self._attributes
|
||||
|
||||
|
||||
@@ -501,6 +501,6 @@ class IcloudDevice:
|
||||
return self._location
|
||||
|
||||
@property
|
||||
def exta_state_attributes(self) -> dict[str, any]:
|
||||
def extra_state_attributes(self) -> dict[str, any]:
|
||||
"""Return the attributes."""
|
||||
return self._attrs
|
||||
|
||||
@@ -110,7 +110,7 @@ class IcloudTrackerEntity(TrackerEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, any]:
|
||||
"""Return the device state attributes."""
|
||||
return self._device.state_attributes
|
||||
return self._device.extra_state_attributes
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, any]:
|
||||
|
||||
@@ -93,7 +93,7 @@ class IcloudDeviceBatterySensor(SensorEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, any]:
|
||||
"""Return default attributes for the iCloud device entity."""
|
||||
return self._device.state_attributes
|
||||
return self._device.extra_state_attributes
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, any]:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -73,6 +73,20 @@ VALID_COLOR_MODES = {
|
||||
COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
|
||||
COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY}
|
||||
|
||||
|
||||
def valid_supported_color_modes(color_modes):
|
||||
"""Validate the given color modes."""
|
||||
color_modes = set(color_modes)
|
||||
if (
|
||||
not color_modes
|
||||
or COLOR_MODE_UNKNOWN in color_modes
|
||||
or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1)
|
||||
or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1)
|
||||
):
|
||||
raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
|
||||
return color_modes
|
||||
|
||||
|
||||
# Float that represents transition time in seconds to make change.
|
||||
ATTR_TRANSITION = "transition"
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import CONF_TRACK_HOME, DOMAIN
|
||||
from .const import (
|
||||
CONF_TRACK_HOME,
|
||||
DEFAULT_HOME_LATITUDE,
|
||||
DEFAULT_HOME_LONGITUDE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
|
||||
|
||||
@@ -35,6 +40,20 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up Met as config entry."""
|
||||
# Don't setup if tracking home location and latitude or longitude isn't set.
|
||||
# Also, filters out our onboarding default location.
|
||||
if config_entry.data.get(CONF_TRACK_HOME, False) and (
|
||||
(not hass.config.latitude and not hass.config.longitude)
|
||||
or (
|
||||
hass.config.latitude == DEFAULT_HOME_LATITUDE
|
||||
and hass.config.longitude == DEFAULT_HOME_LONGITUDE
|
||||
)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Skip setting up met.no integration; No Home location has been set"
|
||||
)
|
||||
return False
|
||||
|
||||
coordinator = MetDataUpdateCoordinator(hass, config_entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -68,7 +87,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.weather = MetWeatherData(
|
||||
hass, config_entry.data, hass.config.units.is_metric
|
||||
)
|
||||
self.weather.init_data()
|
||||
self.weather.set_coordinates()
|
||||
|
||||
update_interval = timedelta(minutes=randrange(55, 65))
|
||||
|
||||
@@ -88,8 +107,8 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
async def _async_update_weather_data(_event=None):
|
||||
"""Update weather data."""
|
||||
self.weather.init_data()
|
||||
await self.async_refresh()
|
||||
if self.weather.set_coordinates():
|
||||
await self.async_refresh()
|
||||
|
||||
self._unsub_track_home = self.hass.bus.async_listen(
|
||||
EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data
|
||||
@@ -114,9 +133,10 @@ class MetWeatherData:
|
||||
self.current_weather_data = {}
|
||||
self.daily_forecast = None
|
||||
self.hourly_forecast = None
|
||||
self._coordinates = None
|
||||
|
||||
def init_data(self):
|
||||
"""Weather data inialization - get the coordinates."""
|
||||
def set_coordinates(self):
|
||||
"""Weather data inialization - set the coordinates."""
|
||||
if self._config.get(CONF_TRACK_HOME, False):
|
||||
latitude = self.hass.config.latitude
|
||||
longitude = self.hass.config.longitude
|
||||
@@ -136,10 +156,14 @@ class MetWeatherData:
|
||||
"lon": str(longitude),
|
||||
"msl": str(elevation),
|
||||
}
|
||||
if coordinates == self._coordinates:
|
||||
return False
|
||||
self._coordinates = coordinates
|
||||
|
||||
self._weather_data = metno.MetWeatherData(
|
||||
coordinates, async_get_clientsession(self.hass), api_url=URL
|
||||
)
|
||||
return True
|
||||
|
||||
async def fetch_data(self):
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
|
||||
@@ -10,7 +10,13 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME
|
||||
from .const import (
|
||||
CONF_TRACK_HOME,
|
||||
DEFAULT_HOME_LATITUDE,
|
||||
DEFAULT_HOME_LONGITUDE,
|
||||
DOMAIN,
|
||||
HOME_LOCATION_NAME,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -81,6 +87,14 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_onboarding(self, data=None):
|
||||
"""Handle a flow initialized by onboarding."""
|
||||
# Don't create entry if latitude or longitude isn't set.
|
||||
# Also, filters out our onboarding default location.
|
||||
if (not self.hass.config.latitude and not self.hass.config.longitude) or (
|
||||
self.hass.config.latitude == DEFAULT_HOME_LATITUDE
|
||||
and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE
|
||||
):
|
||||
return self.async_abort(reason="no_home")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True}
|
||||
)
|
||||
|
||||
@@ -34,6 +34,9 @@ HOME_LOCATION_NAME = "Home"
|
||||
|
||||
CONF_TRACK_HOME = "track_home"
|
||||
|
||||
DEFAULT_HOME_LATITUDE = 52.3731339
|
||||
DEFAULT_HOME_LONGITUDE = 4.8903147
|
||||
|
||||
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}"
|
||||
|
||||
CONDITIONS_MAP = {
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"abort": {
|
||||
"no_home": "No home coordinates are set in the Home Assistant configuration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -183,10 +183,14 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity):
|
||||
if self.coordinator.data is None:
|
||||
return False
|
||||
|
||||
if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]:
|
||||
return False
|
||||
gateway_available = self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]
|
||||
if self._device_type == TYPE_GATEWAY:
|
||||
return gateway_available
|
||||
|
||||
return self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
|
||||
return (
|
||||
gateway_available
|
||||
and self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
||||
@@ -35,6 +35,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_WHITE_VALUE,
|
||||
VALID_COLOR_MODES,
|
||||
LightEntity,
|
||||
valid_supported_color_modes,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
@@ -130,7 +131,10 @@ PLATFORM_SCHEMA_JSON = vol.All(
|
||||
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All(
|
||||
cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique()
|
||||
cv.ensure_list,
|
||||
[vol.In(VALID_COLOR_MODES)],
|
||||
vol.Unique(),
|
||||
valid_supported_color_modes,
|
||||
),
|
||||
vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean,
|
||||
vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
|
||||
@@ -197,7 +201,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS
|
||||
self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP
|
||||
self._supported_features |= config[CONF_HS] and SUPPORT_COLOR
|
||||
self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR
|
||||
self._supported_features |= config[CONF_RGB] and (
|
||||
SUPPORT_COLOR | SUPPORT_BRIGHTNESS
|
||||
)
|
||||
self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE
|
||||
self._supported_features |= config[CONF_XY] and SUPPORT_COLOR
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
and self._templates[CONF_GREEN_TEMPLATE] is not None
|
||||
and self._templates[CONF_BLUE_TEMPLATE] is not None
|
||||
):
|
||||
features = features | SUPPORT_COLOR
|
||||
features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS
|
||||
if self._config.get(CONF_EFFECT_LIST) is not None:
|
||||
features = features | SUPPORT_EFFECT
|
||||
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:
|
||||
|
||||
@@ -70,12 +70,12 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
|
||||
else:
|
||||
amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
|
||||
|
||||
if amount == 0:
|
||||
return CoverState.CLOSED
|
||||
if v_up and not v_down and not v_stop:
|
||||
return CoverState.OPENING
|
||||
if not v_up and v_down and not v_stop:
|
||||
return CoverState.CLOSING
|
||||
if not v_up and not v_down and v_stop and amount == 0:
|
||||
return CoverState.CLOSED
|
||||
return CoverState.OPEN
|
||||
|
||||
@property
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class ProwlNotificationService(BaseNotificationService):
|
||||
"description": message,
|
||||
"priority": data["priority"] if data and "priority" in data else 0,
|
||||
}
|
||||
if data.get("url"):
|
||||
if data and data.get("url"):
|
||||
payload["url"] = data["url"]
|
||||
|
||||
_LOGGER.debug("Attempting call Prowl service at %s", url)
|
||||
|
||||
@@ -363,6 +363,20 @@ def _apply_update(engine, new_version, old_version):
|
||||
if engine.dialect.name == "mysql":
|
||||
_modify_columns(engine, "events", ["event_data LONGTEXT"])
|
||||
_modify_columns(engine, "states", ["attributes LONGTEXT"])
|
||||
elif new_version == 13:
|
||||
if engine.dialect.name == "mysql":
|
||||
_modify_columns(
|
||||
engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"]
|
||||
)
|
||||
_modify_columns(
|
||||
engine,
|
||||
"states",
|
||||
[
|
||||
"last_changed DATETIME(6)",
|
||||
"last_updated DATETIME(6)",
|
||||
"created DATETIME(6)",
|
||||
],
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"No schema migration defined for version {new_version}")
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import homeassistant.util.dt as dt_util
|
||||
# pylint: disable=invalid-name
|
||||
Base = declarative_base()
|
||||
|
||||
SCHEMA_VERSION = 12
|
||||
SCHEMA_VERSION = 13
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,6 +39,10 @@ TABLE_SCHEMA_CHANGES = "schema_changes"
|
||||
|
||||
ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES]
|
||||
|
||||
DATETIME_TYPE = DateTime(timezone=True).with_variant(
|
||||
mysql.DATETIME(timezone=True, fsp=6), "mysql"
|
||||
)
|
||||
|
||||
|
||||
class Events(Base): # type: ignore
|
||||
"""Event history data."""
|
||||
@@ -52,8 +56,8 @@ class Events(Base): # type: ignore
|
||||
event_type = Column(String(32))
|
||||
event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql"))
|
||||
origin = Column(String(32))
|
||||
time_fired = Column(DateTime(timezone=True), index=True)
|
||||
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
|
||||
time_fired = Column(DATETIME_TYPE, index=True)
|
||||
created = Column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
context_id = Column(String(36), index=True)
|
||||
context_user_id = Column(String(36), index=True)
|
||||
context_parent_id = Column(String(36), index=True)
|
||||
@@ -123,9 +127,9 @@ class States(Base): # type: ignore
|
||||
event_id = Column(
|
||||
Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
|
||||
last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True)
|
||||
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
|
||||
last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True)
|
||||
created = Column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
old_state_id = Column(
|
||||
Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True
|
||||
)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Support for binary sensor using RPi GPIO."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import rpi_gpio
|
||||
@@ -52,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class RPiGPIOBinarySensor(BinarySensorEntity):
|
||||
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
||||
|
||||
async def async_read_gpio(self):
|
||||
"""Read state from GPIO."""
|
||||
await asyncio.sleep(float(self._bouncetime) / 1000)
|
||||
self._state = await self.hass.async_add_executor_job(
|
||||
rpi_gpio.read_input, self._port
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
"""Initialize the RPi binary sensor."""
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
@@ -63,12 +74,11 @@ class RPiGPIOBinarySensor(BinarySensorEntity):
|
||||
|
||||
rpi_gpio.setup_input(self._port, self._pull_mode)
|
||||
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.schedule_update_ha_state()
|
||||
def edge_detected(port):
|
||||
"""Edge detection handler."""
|
||||
self.hass.add_job(self.async_read_gpio)
|
||||
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==0.6.1"],
|
||||
"requirements": ["aioshelly==0.6.2"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
|
||||
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"name": "Speedtest.net",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/speedtestdotnet",
|
||||
"requirements": ["speedtest-cli==2.1.2"],
|
||||
"requirements": [
|
||||
"speedtest-cli==2.1.3"
|
||||
],
|
||||
"codeowners": ["@rohankapoorcom", "@engrbm87"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import logging
|
||||
import re
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
@@ -18,6 +19,13 @@ CONF_COLUMN_NAME = "column"
|
||||
CONF_QUERIES = "queries"
|
||||
CONF_QUERY = "query"
|
||||
|
||||
DB_URL_RE = re.compile("//.*:.*@")
|
||||
|
||||
|
||||
def redact_credentials(data):
|
||||
"""Redact credentials from string data."""
|
||||
return DB_URL_RE.sub("//****:****@", data)
|
||||
|
||||
|
||||
def validate_sql_select(value):
|
||||
"""Validate that value is a SQL SELECT query."""
|
||||
@@ -47,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
if not db_url:
|
||||
db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE))
|
||||
|
||||
sess = None
|
||||
try:
|
||||
engine = sqlalchemy.create_engine(db_url)
|
||||
sessmaker = scoped_session(sessionmaker(bind=engine))
|
||||
@@ -56,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
sess.execute("SELECT 1;")
|
||||
|
||||
except sqlalchemy.exc.SQLAlchemyError as err:
|
||||
_LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
|
||||
_LOGGER.error(
|
||||
"Couldn't connect using %s DB_URL: %s",
|
||||
redact_credentials(db_url),
|
||||
redact_credentials(str(err)),
|
||||
)
|
||||
return
|
||||
finally:
|
||||
sess.close()
|
||||
if sess:
|
||||
sess.close()
|
||||
|
||||
queries = []
|
||||
|
||||
@@ -147,7 +161,11 @@ class SQLSensor(SensorEntity):
|
||||
value = str(value)
|
||||
self._attributes[key] = value
|
||||
except sqlalchemy.exc.SQLAlchemyError as err:
|
||||
_LOGGER.error("Error executing query %s: %s", self._query, err)
|
||||
_LOGGER.error(
|
||||
"Error executing query %s: %s",
|
||||
self._query,
|
||||
redact_credentials(str(err)),
|
||||
)
|
||||
return
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
@@ -39,7 +39,12 @@ from .hls import async_setup_hls
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STREAM_SOURCE_RE = re.compile("//(.*):(.*)@")
|
||||
STREAM_SOURCE_RE = re.compile("//.*:.*@")
|
||||
|
||||
|
||||
def redact_credentials(data):
|
||||
"""Redact credentials from string data."""
|
||||
return STREAM_SOURCE_RE.sub("//****:****@", data)
|
||||
|
||||
|
||||
def create_stream(hass, stream_source, options=None):
|
||||
@@ -176,9 +181,7 @@ class Stream:
|
||||
target=self._run_worker,
|
||||
)
|
||||
self._thread.start()
|
||||
_LOGGER.info(
|
||||
"Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
|
||||
)
|
||||
_LOGGER.info("Started stream: %s", redact_credentials(str(self.source)))
|
||||
|
||||
def update_source(self, new_source):
|
||||
"""Restart the stream with a new stream source."""
|
||||
@@ -244,9 +247,7 @@ class Stream:
|
||||
self._thread_quit.set()
|
||||
self._thread.join()
|
||||
self._thread = None
|
||||
_LOGGER.info(
|
||||
"Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
|
||||
)
|
||||
_LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source)))
|
||||
|
||||
async def async_record(self, video_path, duration=30, lookback=5):
|
||||
"""Make a .mp4 recording from a provided stream."""
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
|
||||
import av
|
||||
|
||||
from . import STREAM_SOURCE_RE
|
||||
from . import redact_credentials
|
||||
from .const import (
|
||||
AUDIO_CODECS,
|
||||
MAX_MISSING_DTS,
|
||||
@@ -128,9 +128,7 @@ def stream_worker(source, options, segment_buffer, quit_event):
|
||||
try:
|
||||
container = av.open(source, options=options, timeout=STREAM_TIMEOUT)
|
||||
except av.AVError:
|
||||
_LOGGER.error(
|
||||
"Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source))
|
||||
)
|
||||
_LOGGER.error("Error opening stream %s", redact_credentials(str(source)))
|
||||
return
|
||||
try:
|
||||
video_stream = container.streams.video[0]
|
||||
|
||||
@@ -1,68 +1,131 @@
|
||||
"""The template component."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import CoreState, callback
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
|
||||
from homeassistant.core import CoreState, Event, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
discovery,
|
||||
trigger as trigger_helper,
|
||||
update_coordinator,
|
||||
)
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.reload import async_reload_integration_platforms
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the template integration."""
|
||||
if DOMAIN in config:
|
||||
for conf in config[DOMAIN]:
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(config)
|
||||
await _process_config(hass, config)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
async def _reload_config(call: Event) -> None:
|
||||
"""Reload top-level + platforms."""
|
||||
try:
|
||||
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf = await conf_util.async_process_component_config(
|
||||
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
|
||||
)
|
||||
|
||||
if conf is None:
|
||||
return
|
||||
|
||||
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
if DOMAIN in conf:
|
||||
await _process_config(hass, conf)
|
||||
|
||||
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
|
||||
|
||||
hass.helpers.service.async_register_admin_service(
|
||||
DOMAIN, SERVICE_RELOAD, _reload_config
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _process_config(hass, config):
|
||||
"""Process config."""
|
||||
coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN)
|
||||
|
||||
# Remove old ones
|
||||
if coordinators:
|
||||
for coordinator in coordinators:
|
||||
coordinator.async_remove()
|
||||
|
||||
async def init_coordinator(hass, conf):
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(conf)
|
||||
return coordinator
|
||||
|
||||
hass.data[DOMAIN] = await asyncio.gather(
|
||||
*[init_coordinator(hass, conf) for conf in config[DOMAIN]]
|
||||
)
|
||||
|
||||
|
||||
class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||
"""Class to handle incoming data."""
|
||||
|
||||
REMOVE_TRIGGER = object()
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Instantiate trigger data."""
|
||||
super().__init__(
|
||||
hass, logging.getLogger(__name__), name="Trigger Update Coordinator"
|
||||
)
|
||||
super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
|
||||
self.config = config
|
||||
self._unsub_trigger = None
|
||||
self._unsub_start: Callable[[], None] | None = None
|
||||
self._unsub_trigger: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return unique ID for the entity."""
|
||||
return self.config.get("unique_id")
|
||||
|
||||
@callback
|
||||
def async_remove(self):
|
||||
"""Signal that the entities need to remove themselves."""
|
||||
if self._unsub_start:
|
||||
self._unsub_start()
|
||||
if self._unsub_trigger:
|
||||
self._unsub_trigger()
|
||||
|
||||
async def async_setup(self, hass_config):
|
||||
"""Set up the trigger and create entities."""
|
||||
if self.hass.state == CoreState.running:
|
||||
await self._attach_triggers()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
self._unsub_start = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, self._attach_triggers
|
||||
)
|
||||
|
||||
self.hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
self.hass,
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
{"coordinator": self, "entities": self.config[CONF_SENSORS]},
|
||||
hass_config,
|
||||
for platform_domain in (SENSOR_DOMAIN,):
|
||||
self.hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
self.hass,
|
||||
platform_domain,
|
||||
DOMAIN,
|
||||
{"coordinator": self, "entities": self.config[platform_domain]},
|
||||
hass_config,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _attach_triggers(self, start_event=None) -> None:
|
||||
"""Attach the triggers."""
|
||||
if start_event is not None:
|
||||
self._unsub_start = None
|
||||
|
||||
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
|
||||
self.hass,
|
||||
self.config[CONF_TRIGGER],
|
||||
|
||||
@@ -1,49 +1,128 @@
|
||||
"""Template config validator."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
)
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_SENSORS,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.trigger import async_validate_trigger_config
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN
|
||||
from .sensor import SENSOR_SCHEMA
|
||||
from .const import (
|
||||
CONF_ATTRIBUTE_TEMPLATES,
|
||||
CONF_ATTRIBUTES,
|
||||
CONF_AVAILABILITY,
|
||||
CONF_AVAILABILITY_TEMPLATE,
|
||||
CONF_PICTURE,
|
||||
CONF_TRIGGER,
|
||||
DOMAIN,
|
||||
)
|
||||
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
||||
|
||||
CONF_STATE = "state"
|
||||
LEGACY_SENSOR = {
|
||||
CONF_ICON_TEMPLATE: CONF_ICON,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
||||
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
||||
CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
|
||||
CONF_FRIENDLY_NAME: CONF_NAME,
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
|
||||
TRIGGER_ENTITY_SCHEMA = vol.Schema(
|
||||
SENSOR_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_AVAILABILITY): cv.template,
|
||||
vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
|
||||
vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
|
||||
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _rewrite_legacy_to_modern_trigger_conf(cfg: dict):
|
||||
"""Rewrite a legacy to a modern trigger-basd conf."""
|
||||
logging.getLogger(__name__).warning(
|
||||
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
|
||||
)
|
||||
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
|
||||
|
||||
for device_id, entity_cfg in cfg[CONF_SENSORS].items():
|
||||
entity_cfg = {**entity_cfg}
|
||||
|
||||
for from_key, to_key in LEGACY_SENSOR.items():
|
||||
if from_key not in entity_cfg or to_key in entity_cfg:
|
||||
continue
|
||||
|
||||
val = entity_cfg.pop(from_key)
|
||||
if isinstance(val, str):
|
||||
val = template.Template(val)
|
||||
entity_cfg[to_key] = val
|
||||
|
||||
if CONF_NAME not in entity_cfg:
|
||||
entity_cfg[CONF_NAME] = template.Template(device_id)
|
||||
|
||||
sensor.append(entity_cfg)
|
||||
|
||||
return {**cfg, "sensor": sensor}
|
||||
|
||||
|
||||
async def async_validate_config(hass, config):
|
||||
"""Validate config."""
|
||||
if DOMAIN not in config:
|
||||
return config
|
||||
|
||||
trigger_entity_configs = []
|
||||
config_sections = []
|
||||
|
||||
for cfg in cv.ensure_list(config[DOMAIN]):
|
||||
try:
|
||||
cfg = TRIGGER_ENTITY_SCHEMA(cfg)
|
||||
cfg = CONFIG_SECTION_SCHEMA(cfg)
|
||||
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
hass, cfg[CONF_TRIGGER]
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
async_log_exception(err, DOMAIN, cfg, hass)
|
||||
continue
|
||||
|
||||
else:
|
||||
trigger_entity_configs.append(cfg)
|
||||
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
|
||||
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
|
||||
|
||||
config_sections.append(cfg)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
config = config_without_domain(config, DOMAIN)
|
||||
config[DOMAIN] = trigger_entity_configs
|
||||
config[DOMAIN] = config_sections
|
||||
|
||||
return config
|
||||
|
||||
@@ -20,3 +20,7 @@ PLATFORMS = [
|
||||
"vacuum",
|
||||
"weather",
|
||||
]
|
||||
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_PICTURE = "picture"
|
||||
|
||||
@@ -5,6 +5,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
@@ -17,6 +18,7 @@ from homeassistant.const import (
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_SENSORS,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -88,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config):
|
||||
friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
|
||||
unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
|
||||
attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
|
||||
unique_id = device_config.get(CONF_UNIQUE_ID)
|
||||
|
||||
sensors.append(
|
||||
@@ -117,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
async_add_entities(_async_create_template_tracking_entities(hass, config))
|
||||
else:
|
||||
async_add_entities(
|
||||
TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config)
|
||||
for device_id, config in discovery_info["entities"].items()
|
||||
TriggerSensorEntity(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
)
|
||||
|
||||
|
||||
@@ -201,9 +203,10 @@ class SensorTemplate(TemplateEntity, SensorEntity):
|
||||
class TriggerSensorEntity(TriggerEntity, SensorEntity):
|
||||
"""Sensor entity based on trigger data."""
|
||||
|
||||
extra_template_keys = (CONF_VALUE_TEMPLATE,)
|
||||
domain = SENSOR_DOMAIN
|
||||
extra_template_keys = (CONF_STATE,)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return state of the sensor."""
|
||||
return self._rendered.get(CONF_VALUE_TEMPLATE)
|
||||
return self._rendered.get(CONF_STATE)
|
||||
|
||||
@@ -6,20 +6,16 @@ from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import template, update_coordinator
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
|
||||
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
|
||||
|
||||
|
||||
class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
@@ -32,23 +28,13 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
device_id: str,
|
||||
config: dict,
|
||||
):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_id = async_generate_entity_id(
|
||||
self.domain + ".{}", device_id, hass=hass
|
||||
)
|
||||
|
||||
self._name = config.get(CONF_FRIENDLY_NAME, device_id)
|
||||
|
||||
entity_unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
if entity_unique_id is None and coordinator.unique_id:
|
||||
entity_unique_id = device_id
|
||||
|
||||
if entity_unique_id and coordinator.unique_id:
|
||||
self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}"
|
||||
else:
|
||||
@@ -56,32 +42,33 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
|
||||
self._config = config
|
||||
|
||||
self._to_render = [
|
||||
itm
|
||||
for itm in (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_AVAILABILITY_TEMPLATE,
|
||||
)
|
||||
if itm in config
|
||||
]
|
||||
self._static_rendered = {}
|
||||
self._to_render = []
|
||||
|
||||
for itm in (
|
||||
CONF_NAME,
|
||||
CONF_ICON,
|
||||
CONF_PICTURE,
|
||||
CONF_AVAILABILITY,
|
||||
):
|
||||
if itm not in config:
|
||||
continue
|
||||
|
||||
if config[itm].is_static:
|
||||
self._static_rendered[itm] = config[itm].template
|
||||
else:
|
||||
self._to_render.append(itm)
|
||||
|
||||
if self.extra_template_keys is not None:
|
||||
self._to_render.extend(self.extra_template_keys)
|
||||
|
||||
self._rendered = {}
|
||||
# We make a copy so our initial render is 'unknown' and not 'unavailable'
|
||||
self._rendered = dict(self._static_rendered)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the entity."""
|
||||
if (
|
||||
self._rendered is not None
|
||||
and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None
|
||||
):
|
||||
return name
|
||||
return self._name
|
||||
return self._rendered.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -101,29 +88,27 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return icon."""
|
||||
return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE)
|
||||
return self._rendered.get(CONF_ICON)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return entity picture."""
|
||||
return self._rendered is not None and self._rendered.get(
|
||||
CONF_ENTITY_PICTURE_TEMPLATE
|
||||
)
|
||||
return self._rendered.get(CONF_PICTURE)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return availability of the entity."""
|
||||
return (
|
||||
self._rendered is not None
|
||||
self._rendered is not self._static_rendered
|
||||
and
|
||||
# Check against False so `None` is ok
|
||||
self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False
|
||||
self._rendered.get(CONF_AVAILABILITY) is not False
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return extra attributes."""
|
||||
return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES)
|
||||
return self._rendered.get(CONF_ATTRIBUTES)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle being added to Home Assistant."""
|
||||
@@ -136,16 +121,16 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
rendered = {}
|
||||
rendered = dict(self._static_rendered)
|
||||
|
||||
for key in self._to_render:
|
||||
rendered[key] = self._config[key].async_render(
|
||||
self.coordinator.data["run_variables"], parse_result=False
|
||||
)
|
||||
|
||||
if CONF_ATTRIBUTE_TEMPLATES in self._config:
|
||||
rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex(
|
||||
self._config[CONF_ATTRIBUTE_TEMPLATES],
|
||||
if CONF_ATTRIBUTES in self._config:
|
||||
rendered[CONF_ATTRIBUTES] = template.render_complex(
|
||||
self._config[CONF_ATTRIBUTES],
|
||||
self.coordinator.data["run_variables"],
|
||||
)
|
||||
|
||||
@@ -154,7 +139,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
|
||||
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
||||
)
|
||||
self._rendered = None
|
||||
self._rendered = self._static_rendered
|
||||
|
||||
self.async_set_context(self.coordinator.data["context"])
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,7 +112,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
self._state = ALARM_STATE_TO_HA.get(
|
||||
self.coordinator.data["alarm"]["statusType"]
|
||||
)
|
||||
self._changed_by = self.coordinator.data["alarm"]["name"]
|
||||
self._changed_by = self.coordinator.data["alarm"].get("name")
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
||||
@@ -36,7 +36,7 @@ async def async_setup_entry(
|
||||
|
||||
assert hass.config.config_dir
|
||||
async_add_entities(
|
||||
VerisureSmartcam(hass, coordinator, serial_number, hass.config.config_dir)
|
||||
VerisureSmartcam(coordinator, serial_number, hass.config.config_dir)
|
||||
for serial_number in coordinator.data["cameras"]
|
||||
)
|
||||
|
||||
@@ -48,19 +48,18 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: VerisureDataUpdateCoordinator,
|
||||
serial_number: str,
|
||||
directory_path: str,
|
||||
):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__(coordinator)
|
||||
Camera.__init__(self)
|
||||
|
||||
self.serial_number = serial_number
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -126,7 +125,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
|
||||
self._image_id = new_image_id
|
||||
self._image = new_image_path
|
||||
|
||||
def delete_image(self) -> None:
|
||||
def delete_image(self, _=None) -> None:
|
||||
"""Delete an old image."""
|
||||
remove_image = os.path.join(
|
||||
self._directory_path, "{}{}".format(self._image_id, ".jpg")
|
||||
@@ -145,3 +144,8 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
|
||||
LOGGER.debug("Capturing new image from %s", self.serial_number)
|
||||
except VerisureError as ex:
|
||||
LOGGER.error("Could not capture image, %s", ex)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)
|
||||
|
||||
@@ -391,6 +391,9 @@ class PollControl(ZigbeeChannel):
|
||||
CHECKIN_INTERVAL = 55 * 60 * 4 # 55min
|
||||
CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s
|
||||
LONG_POLL = 6 * 4 # 6s
|
||||
_IGNORED_MANUFACTURER_ID = {
|
||||
4476,
|
||||
} # IKEA
|
||||
|
||||
async def async_configure_channel_specific(self) -> None:
|
||||
"""Configure channel: set check-in interval."""
|
||||
@@ -416,7 +419,13 @@ class PollControl(ZigbeeChannel):
|
||||
async def check_in_response(self, tsn: int) -> None:
|
||||
"""Respond to checkin command."""
|
||||
await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn)
|
||||
await self.set_long_poll_interval(self.LONG_POLL)
|
||||
if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID:
|
||||
await self.set_long_poll_interval(self.LONG_POLL)
|
||||
|
||||
@callback
|
||||
def skip_manufacturer_id(self, manufacturer_code: int) -> None:
|
||||
"""Block a specific manufacturer id from changing default polling."""
|
||||
self._IGNORED_MANUFACTURER_ID.add(manufacturer_code)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id)
|
||||
|
||||
@@ -169,11 +169,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
THERMOSTAT_MODE_PROPERTY,
|
||||
CommandClass.THERMOSTAT_FAN_MODE,
|
||||
add_to_watched_value_ids=True,
|
||||
check_all_endpoints=True,
|
||||
)
|
||||
self._fan_state = self.get_zwave_value(
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
CommandClass.THERMOSTAT_FAN_STATE,
|
||||
add_to_watched_value_ids=True,
|
||||
check_all_endpoints=True,
|
||||
)
|
||||
self._set_modes_and_presets()
|
||||
self._supported_features = 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.23.0"],
|
||||
"requirements": ["zwave-js-server-python==0.23.1"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["http", "websocket_api"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import weakref
|
||||
import attr
|
||||
|
||||
from homeassistant import data_entry_flow, loader
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
from homeassistant.helpers.event import Event
|
||||
@@ -276,14 +277,19 @@ class ConfigEntry:
|
||||
wait_time,
|
||||
)
|
||||
|
||||
async def setup_again(now: Any) -> None:
|
||||
async def setup_again(*_: Any) -> None:
|
||||
"""Run setup again."""
|
||||
self._async_cancel_retry_setup = None
|
||||
await self.async_setup(hass, integration=integration, tries=tries)
|
||||
|
||||
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
|
||||
wait_time, setup_again
|
||||
)
|
||||
if hass.state == CoreState.running:
|
||||
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
|
||||
wait_time, setup_again
|
||||
)
|
||||
else:
|
||||
self._async_cancel_retry_setup = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, setup_again
|
||||
)
|
||||
return
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 4
|
||||
PATCH_VERSION = "0b0"
|
||||
PATCH_VERSION = "1"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 8, 0)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import base64
|
||||
import collections.abc
|
||||
from contextlib import suppress
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial, wraps
|
||||
import json
|
||||
@@ -79,6 +80,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = {
|
||||
ALL_STATES_RATE_LIMIT = timedelta(minutes=1)
|
||||
DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1)
|
||||
|
||||
template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def attach(hass: HomeAssistant, obj: Any) -> None:
|
||||
@@ -299,7 +302,7 @@ class Template:
|
||||
|
||||
self.template: str = template.strip()
|
||||
self._compiled_code = None
|
||||
self._compiled: Template | None = None
|
||||
self._compiled: jinja2.Template | None = None
|
||||
self.hass = hass
|
||||
self.is_static = not is_template_string(template)
|
||||
self._limited = None
|
||||
@@ -336,7 +339,7 @@ class Template:
|
||||
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
|
||||
"""
|
||||
if self.is_static:
|
||||
if self.hass.config.legacy_templates or not parse_result:
|
||||
if not parse_result or self.hass.config.legacy_templates:
|
||||
return self.template
|
||||
return self._parse_result(self.template)
|
||||
|
||||
@@ -360,7 +363,7 @@ class Template:
|
||||
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
|
||||
"""
|
||||
if self.is_static:
|
||||
if self.hass.config.legacy_templates or not parse_result:
|
||||
if not parse_result or self.hass.config.legacy_templates:
|
||||
return self.template
|
||||
return self._parse_result(self.template)
|
||||
|
||||
@@ -370,7 +373,7 @@ class Template:
|
||||
kwargs.update(variables)
|
||||
|
||||
try:
|
||||
render_result = compiled.render(kwargs)
|
||||
render_result = _render_with_context(self.template, compiled, **kwargs)
|
||||
except Exception as err:
|
||||
raise TemplateError(err) from err
|
||||
|
||||
@@ -442,7 +445,7 @@ class Template:
|
||||
|
||||
def _render_template() -> None:
|
||||
try:
|
||||
compiled.render(kwargs)
|
||||
_render_with_context(self.template, compiled, **kwargs)
|
||||
except TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
@@ -524,7 +527,9 @@ class Template:
|
||||
variables["value_json"] = json.loads(value)
|
||||
|
||||
try:
|
||||
return self._compiled.render(variables).strip()
|
||||
return _render_with_context(
|
||||
self.template, self._compiled, **variables
|
||||
).strip()
|
||||
except jinja2.TemplateError as ex:
|
||||
if error_value is _SENTINEL:
|
||||
_LOGGER.error(
|
||||
@@ -535,7 +540,7 @@ class Template:
|
||||
)
|
||||
return value if error_value is _SENTINEL else error_value
|
||||
|
||||
def _ensure_compiled(self, limited: bool = False) -> Template:
|
||||
def _ensure_compiled(self, limited: bool = False) -> jinja2.Template:
|
||||
"""Bind a template to a specific hass instance."""
|
||||
self.ensure_valid()
|
||||
|
||||
@@ -548,7 +553,7 @@ class Template:
|
||||
env = self._env
|
||||
|
||||
self._compiled = cast(
|
||||
Template,
|
||||
jinja2.Template,
|
||||
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
|
||||
)
|
||||
|
||||
@@ -1314,12 +1319,59 @@ def urlencode(value):
|
||||
return urllib_urlencode(value).encode("utf-8")
|
||||
|
||||
|
||||
def _render_with_context(
|
||||
template_str: str, template: jinja2.Template, **kwargs: Any
|
||||
) -> str:
|
||||
"""Store template being rendered in a ContextVar to aid error handling."""
|
||||
template_cv.set(template_str)
|
||||
return template.render(**kwargs)
|
||||
|
||||
|
||||
class LoggingUndefined(jinja2.Undefined):
|
||||
"""Log on undefined variables."""
|
||||
|
||||
def _log_message(self):
|
||||
template = template_cv.get() or ""
|
||||
_LOGGER.warning(
|
||||
"Template variable warning: %s when rendering '%s'",
|
||||
self._undefined_message,
|
||||
template,
|
||||
)
|
||||
|
||||
def _fail_with_undefined_error(self, *args, **kwargs):
|
||||
try:
|
||||
return super()._fail_with_undefined_error(*args, **kwargs)
|
||||
except self._undefined_exception as ex:
|
||||
template = template_cv.get() or ""
|
||||
_LOGGER.error(
|
||||
"Template variable error: %s when rendering '%s'",
|
||||
self._undefined_message,
|
||||
template,
|
||||
)
|
||||
raise ex
|
||||
|
||||
def __str__(self):
|
||||
"""Log undefined __str___."""
|
||||
self._log_message()
|
||||
return super().__str__()
|
||||
|
||||
def __iter__(self):
|
||||
"""Log undefined __iter___."""
|
||||
self._log_message()
|
||||
return super().__iter__()
|
||||
|
||||
def __bool__(self):
|
||||
"""Log undefined __bool___."""
|
||||
self._log_message()
|
||||
return super().__bool__()
|
||||
|
||||
|
||||
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
"""The Home Assistant template environment."""
|
||||
|
||||
def __init__(self, hass, limited=False):
|
||||
"""Initialise template environment."""
|
||||
super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER))
|
||||
super().__init__(undefined=LoggingUndefined)
|
||||
self.hass = hass
|
||||
self.template_cache = weakref.WeakValueDictionary()
|
||||
self.filters["round"] = forgiving_round
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
PyJWT==1.7.1
|
||||
PyNaCl==1.3.0
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
aiohttp==3.7.4.post0
|
||||
aiohttp_cors==0.7.0
|
||||
astral==1.10.1
|
||||
@@ -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==20210407.2
|
||||
httpx==0.17.1
|
||||
jinja2>=2.11.3
|
||||
netdisco==2.8.2
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ aioazuredevops==1.3.5
|
||||
aiobotocore==0.11.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -224,7 +224,7 @@ aiopylgtv==0.4.0
|
||||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.6.1
|
||||
aioshelly==0.6.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
@@ -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==20210407.2
|
||||
|
||||
# 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
|
||||
@@ -1304,7 +1304,7 @@ pycfdns==1.2.1
|
||||
pychannels==1.0.0
|
||||
|
||||
# homeassistant.components.cast
|
||||
pychromecast==9.1.1
|
||||
pychromecast==9.1.2
|
||||
|
||||
# homeassistant.components.pocketcasts
|
||||
pycketcasts==1.0.0
|
||||
@@ -2108,7 +2108,7 @@ sonarr==0.3.0
|
||||
speak2mary==1.4.0
|
||||
|
||||
# homeassistant.components.speedtestdotnet
|
||||
speedtest-cli==2.1.2
|
||||
speedtest-cli==2.1.3
|
||||
|
||||
# homeassistant.components.spider
|
||||
spiderpy==1.4.2
|
||||
@@ -2402,4 +2402,4 @@ zigpy==0.33.0
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.23.0
|
||||
zwave-js-server-python==0.23.1
|
||||
|
||||
@@ -84,7 +84,7 @@ aioazuredevops==1.3.5
|
||||
aiobotocore==0.11.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -143,7 +143,7 @@ aiopylgtv==0.4.0
|
||||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.6.1
|
||||
aioshelly==0.6.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
@@ -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==20210407.2
|
||||
|
||||
# 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
|
||||
@@ -690,7 +690,7 @@ pybotvac==0.0.20
|
||||
pycfdns==1.2.1
|
||||
|
||||
# homeassistant.components.cast
|
||||
pychromecast==9.1.1
|
||||
pychromecast==9.1.2
|
||||
|
||||
# homeassistant.components.climacell
|
||||
pyclimacell==0.14.0
|
||||
@@ -1095,7 +1095,7 @@ sonarr==0.3.0
|
||||
speak2mary==1.4.0
|
||||
|
||||
# homeassistant.components.speedtestdotnet
|
||||
speedtest-cli==2.1.2
|
||||
speedtest-cli==2.1.3
|
||||
|
||||
# homeassistant.components.spider
|
||||
spiderpy==1.4.2
|
||||
@@ -1251,4 +1251,4 @@ zigpy-znp==0.4.0
|
||||
zigpy==0.33.0
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.23.0
|
||||
zwave-js-server-python==0.23.1
|
||||
|
||||
@@ -127,7 +127,7 @@ async def test_aemet_weather_create_sensors(hass):
|
||||
assert state.state == "Getafe"
|
||||
|
||||
state = hass.states.get("sensor.aemet_town_timestamp")
|
||||
assert state.state == "2021-01-09 11:47:45+00:00"
|
||||
assert state.state == "2021-01-09T11:47:45+00:00"
|
||||
|
||||
state = hass.states.get("sensor.aemet_wind_bearing")
|
||||
assert state.state == "90.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""The tests for the analytics ."""
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
@@ -13,10 +13,12 @@ from homeassistant.components.analytics.const import (
|
||||
ATTR_STATISTICS,
|
||||
ATTR_USAGE,
|
||||
)
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
from homeassistant.components.api import ATTR_UUID
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.loader import IntegrationNotFound
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
MOCK_HUUID = "abcdefg"
|
||||
MOCK_UUID = "abcdefg"
|
||||
|
||||
|
||||
async def test_no_send(hass, caplog, aioclient_mock):
|
||||
@@ -26,8 +28,7 @@ async def test_no_send(hass, caplog, aioclient_mock):
|
||||
with patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=False),
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.load()
|
||||
):
|
||||
assert not analytics.preferences[ATTR_BASE]
|
||||
|
||||
await analytics.send_analytics()
|
||||
@@ -76,9 +77,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock):
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert "Sending analytics failed with statuscode 400" in caplog.text
|
||||
|
||||
|
||||
@@ -88,9 +87,7 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock):
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert "Error sending analytics" in caplog.text
|
||||
|
||||
|
||||
@@ -98,12 +95,15 @@ async def test_send_base(hass, caplog, aioclient_mock):
|
||||
"""Test send base prefrences are defined."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.send_analytics()
|
||||
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||
|
||||
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
|
||||
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||
assert "'installation_type':" in caplog.text
|
||||
assert "'integration_count':" not in caplog.text
|
||||
@@ -131,10 +131,14 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock):
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
"uuid.UUID.hex", new_callable=PropertyMock
|
||||
) as hex:
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.load()
|
||||
|
||||
await analytics.send_analytics()
|
||||
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||
|
||||
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
|
||||
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||
assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text
|
||||
assert "'installation_type':" in caplog.text
|
||||
@@ -147,12 +151,13 @@ async def test_send_usage(hass, caplog, aioclient_mock):
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
assert analytics.preferences[ATTR_USAGE]
|
||||
hass.config.components = ["default_config"]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert "'integrations': ['default_config']" in caplog.text
|
||||
assert "'integration_count':" not in caplog.text
|
||||
|
||||
@@ -195,8 +200,6 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock):
|
||||
), patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
assert (
|
||||
@@ -215,8 +218,7 @@ async def test_send_statistics(hass, caplog, aioclient_mock):
|
||||
assert analytics.preferences[ATTR_STATISTICS]
|
||||
hass.config.components = ["default_config"]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert (
|
||||
"'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0"
|
||||
in caplog.text
|
||||
@@ -236,11 +238,11 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.async_get_integration",
|
||||
side_effect=IntegrationNotFound("any"),
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
|
||||
post_call = aioclient_mock.mock_calls[0]
|
||||
assert "huuid" in post_call[2]
|
||||
assert "uuid" in post_call[2]
|
||||
assert post_call[2]["integration_count"] == 0
|
||||
|
||||
|
||||
@@ -258,7 +260,7 @@ async def test_send_statistics_async_get_integration_unknown_exception(
|
||||
with pytest.raises(ValueError), patch(
|
||||
"homeassistant.components.analytics.analytics.async_get_integration",
|
||||
side_effect=ValueError,
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
|
||||
|
||||
@@ -298,9 +300,36 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock):
|
||||
), patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
assert "'addon_count': 1" in caplog.text
|
||||
assert "'integrations':" not in caplog.text
|
||||
|
||||
|
||||
async def test_reusing_uuid(hass, aioclient_mock):
|
||||
"""Test reusing the stored UUID."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
analytics._data[ATTR_UUID] = "NOT_MOCK_UUID"
|
||||
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
|
||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
|
||||
# This is not actually called but that in itself prove the test
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert analytics.uuid == "NOT_MOCK_UUID"
|
||||
|
||||
|
||||
async def test_custom_integrations(hass, aioclient_mock):
|
||||
"""Test sending custom integrations."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
assert await async_setup_component(hass, "test_package", {"test_package": {}})
|
||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||
|
||||
await analytics.send_analytics()
|
||||
|
||||
payload = aioclient_mock.mock_calls[0][2]
|
||||
assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""The tests for the analytics ."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -22,11 +20,9 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json({"id": 1, "type": "analytics"})
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||
response = await ws_client.receive_json()
|
||||
response = await ws_client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"]["huuid"] == "abcdef"
|
||||
|
||||
await ws_client.send_json(
|
||||
{"id": 2, "type": "analytics/preferences", "preferences": {"base": True}}
|
||||
@@ -36,7 +32,5 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
|
||||
assert response["result"]["preferences"]["base"]
|
||||
|
||||
await ws_client.send_json({"id": 3, "type": "analytics"})
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||
response = await ws_client.receive_json()
|
||||
response = await ws_client.receive_json()
|
||||
assert response["result"]["preferences"]["base"]
|
||||
assert response["result"]["huuid"] == "abcdef"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Test the DoorBird config flow."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
import urllib
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN
|
||||
@@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None):
|
||||
doorbirdapi_mock = MagicMock()
|
||||
type(doorbirdapi_mock).ready = MagicMock(return_value=ready)
|
||||
type(doorbirdapi_mock).info = MagicMock(return_value=info)
|
||||
|
||||
type(doorbirdapi_mock).doorbell_state = MagicMock(
|
||||
side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401))
|
||||
)
|
||||
return doorbirdapi_mock
|
||||
|
||||
|
||||
@@ -137,17 +141,25 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
|
||||
)
|
||||
# Running the zeroconf init will make the unique id
|
||||
# in progress
|
||||
zero_conf = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||
"host": "192.168.1.5",
|
||||
},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
):
|
||||
zero_conf = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||
"host": "192.168.1.5",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert zero_conf["step_id"] == "user"
|
||||
assert zero_conf["errors"] == {}
|
||||
@@ -159,9 +171,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
|
||||
CONF_CUSTOM_URL
|
||||
] = "http://legacy.custom.url/should/only/come/in/from/yaml"
|
||||
|
||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
@@ -244,24 +253,29 @@ async def test_form_zeroconf_correct_oui(hass):
|
||||
await hass.async_add_executor_job(
|
||||
init_recorder_component, hass
|
||||
) # force in memory db
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||
"host": "192.168.1.5",
|
||||
},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||
)
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||
"host": "192.168.1.5",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
@@ -288,6 +302,43 @@ async def test_form_zeroconf_correct_oui(hass):
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"doorbell_state_side_effect",
|
||||
[
|
||||
requests.exceptions.HTTPError(response=Mock(status_code=404)),
|
||||
OSError,
|
||||
None,
|
||||
],
|
||||
)
|
||||
async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect):
|
||||
"""Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
|
||||
await hass.async_add_executor_job(
|
||||
init_recorder_component, hass
|
||||
) # force in memory db
|
||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||
)
|
||||
type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect)
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||
"host": "192.168.1.5",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "not_doorbird_device"
|
||||
|
||||
|
||||
async def test_form_user_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
await hass.async_add_executor_job(
|
||||
@@ -322,10 +373,8 @@ async def test_form_user_invalid_auth(hass):
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_urllib_error = urllib.error.HTTPError(
|
||||
"http://xyz.tld", 401, "login failed", {}, None
|
||||
)
|
||||
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error)
|
||||
mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401))
|
||||
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=doorbirdapi,
|
||||
|
||||
@@ -331,9 +331,18 @@ async def test_sensor_bad_value(hass, setup_comp_2):
|
||||
|
||||
_setup_sensor(hass, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY)
|
||||
assert temp == state.attributes.get("current_temperature")
|
||||
assert state.attributes.get("current_temperature") == temp
|
||||
|
||||
_setup_sensor(hass, "inf")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(ENTITY)
|
||||
assert state.attributes.get("current_temperature") == temp
|
||||
|
||||
_setup_sensor(hass, "nan")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(ENTITY)
|
||||
assert state.attributes.get("current_temperature") == temp
|
||||
|
||||
|
||||
async def test_sensor_unknown(hass):
|
||||
|
||||
@@ -18,7 +18,7 @@ async def test_config_flow(hass):
|
||||
DOMAIN, context={"source": "system"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == DOMAIN.title()
|
||||
assert result["title"] == "Supervisor"
|
||||
assert result["data"] == {}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
"""Tests for Met.no."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.met.const import DOMAIN
|
||||
from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def init_integration(hass) -> MockConfigEntry:
|
||||
async def init_integration(hass, track_home=False) -> MockConfigEntry:
|
||||
"""Set up the Met integration in Home Assistant."""
|
||||
entry_data = {
|
||||
CONF_NAME: "test",
|
||||
CONF_LATITUDE: 0,
|
||||
CONF_LONGITUDE: 0,
|
||||
CONF_ELEVATION: 0,
|
||||
CONF_LONGITUDE: 1.0,
|
||||
CONF_ELEVATION: 1.0,
|
||||
}
|
||||
|
||||
if track_home:
|
||||
entry_data = {CONF_TRACK_HOME: True}
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
|
||||
with patch(
|
||||
"homeassistant.components.met.metno.MetWeatherData.fetching_data",
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -106,6 +107,25 @@ async def test_onboarding_step(hass):
|
||||
assert result["data"] == {"track_home": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("latitude,longitude", [(52.3731339, 4.8903147), (0.0, 0.0)])
|
||||
async def test_onboarding_step_abort_no_home(hass, latitude, longitude):
|
||||
"""Test entry not created when default step fails."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"latitude": latitude, "longitude": longitude},
|
||||
)
|
||||
|
||||
assert hass.config.latitude == latitude
|
||||
assert hass.config.longitude == longitude
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "onboarding"}, data={}
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "no_home"
|
||||
|
||||
|
||||
async def test_import_step(hass):
|
||||
"""Test initializing via import step."""
|
||||
test_data = {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
"""Test the Met integration init."""
|
||||
from homeassistant.components.met.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
|
||||
from homeassistant.components.met.const import (
|
||||
DEFAULT_HOME_LATITUDE,
|
||||
DEFAULT_HOME_LONGITUDE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
ENTRY_STATE_NOT_LOADED,
|
||||
ENTRY_STATE_SETUP_ERROR,
|
||||
)
|
||||
|
||||
from . import init_integration
|
||||
|
||||
@@ -17,3 +26,24 @@ async def test_unload_entry(hass):
|
||||
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_fail_default_home_entry(hass, caplog):
|
||||
"""Test abort setup of default home location."""
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"latitude": 52.3731339, "longitude": 4.8903147},
|
||||
)
|
||||
|
||||
assert hass.config.latitude == DEFAULT_HOME_LATITUDE
|
||||
assert hass.config.longitude == DEFAULT_HOME_LONGITUDE
|
||||
|
||||
entry = await init_integration(hass, track_home=True)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ENTRY_STATE_SETUP_ERROR
|
||||
|
||||
assert (
|
||||
"Skip setting up met.no integration; No Home location has been set"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
@@ -29,6 +29,11 @@ async def test_tracking_home(hass, mock_weather):
|
||||
|
||||
assert len(mock_weather.mock_calls) == 8
|
||||
|
||||
# Same coordinates again should not trigger any new requests to met.no
|
||||
await hass.config.async_update(latitude=10, longitude=20)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_weather.mock_calls) == 8
|
||||
|
||||
entry = hass.config_entries.async_entries()[0]
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -161,7 +161,13 @@ import pytest
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
from homeassistant.components import light
|
||||
from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
SERVICE_RELOAD,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -206,6 +212,27 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock):
|
||||
assert hass.states.get("light.test") is None
|
||||
|
||||
|
||||
async def test_rgb_light(hass, mqtt_mock):
|
||||
"""Test RGB light flags brightness support."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
light.DOMAIN,
|
||||
{
|
||||
light.DOMAIN: {
|
||||
"platform": "mqtt",
|
||||
"name": "test",
|
||||
"command_topic": "test_light_rgb/set",
|
||||
"rgb_command_topic": "test_light_rgb/rgb/set",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.test")
|
||||
expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS
|
||||
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
|
||||
|
||||
|
||||
async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock):
|
||||
"""Test if there is no color and brightness if no topic."""
|
||||
assert await async_setup_component(
|
||||
|
||||
@@ -188,6 +188,60 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated):
|
||||
assert hass.states.get("light.test") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]]
|
||||
)
|
||||
async def test_fail_setup_if_color_modes_invalid(
|
||||
hass, mqtt_mock, supported_color_modes
|
||||
):
|
||||
"""Test if setup fails if supported color modes is invalid."""
|
||||
config = {
|
||||
light.DOMAIN: {
|
||||
"brightness": True,
|
||||
"color_mode": True,
|
||||
"command_topic": "test_light_rgb/set",
|
||||
"name": "test",
|
||||
"platform": "mqtt",
|
||||
"schema": "json",
|
||||
"supported_color_modes": supported_color_modes,
|
||||
}
|
||||
}
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
light.DOMAIN,
|
||||
config,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("light.test") is None
|
||||
|
||||
|
||||
async def test_rgb_light(hass, mqtt_mock):
|
||||
"""Test RGB light flags brightness support."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
light.DOMAIN,
|
||||
{
|
||||
light.DOMAIN: {
|
||||
"platform": "mqtt",
|
||||
"schema": "json",
|
||||
"name": "test",
|
||||
"command_topic": "test_light_rgb/set",
|
||||
"rgb": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.test")
|
||||
expected_features = (
|
||||
light.SUPPORT_TRANSITION
|
||||
| light.SUPPORT_COLOR
|
||||
| light.SUPPORT_FLASH
|
||||
| light.SUPPORT_BRIGHTNESS
|
||||
)
|
||||
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
|
||||
|
||||
|
||||
async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock):
|
||||
"""Test for no RGB, brightness, color temp, effect, white val or XY."""
|
||||
assert await async_setup_component(
|
||||
|
||||
@@ -141,6 +141,37 @@ async def test_setup_fails(hass, mqtt_mock):
|
||||
assert hass.states.get("light.test") is None
|
||||
|
||||
|
||||
async def test_rgb_light(hass, mqtt_mock):
|
||||
"""Test RGB light flags brightness support."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
light.DOMAIN,
|
||||
{
|
||||
light.DOMAIN: {
|
||||
"platform": "mqtt",
|
||||
"schema": "template",
|
||||
"name": "test",
|
||||
"command_topic": "test_light_rgb/set",
|
||||
"command_on_template": "on",
|
||||
"command_off_template": "off",
|
||||
"red_template": '{{ value.split(",")[4].' 'split("-")[0] }}',
|
||||
"green_template": '{{ value.split(",")[4].' 'split("-")[1] }}',
|
||||
"blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}',
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.test")
|
||||
expected_features = (
|
||||
light.SUPPORT_TRANSITION
|
||||
| light.SUPPORT_COLOR
|
||||
| light.SUPPORT_FLASH
|
||||
| light.SUPPORT_BRIGHTNESS
|
||||
)
|
||||
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
|
||||
|
||||
|
||||
async def test_state_change_via_topic(hass, mqtt_mock):
|
||||
"""Test state change via topic."""
|
||||
with assert_setup_component(1, light.DOMAIN):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user