forked from home-assistant/core
Compare commits
55 Commits
2021.4.0b4
...
2021.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b7cfd71de | ||
|
|
d5d9a5ff11 | ||
|
|
3f744bcbef | ||
|
|
92746aa60c | ||
|
|
4da77b9768 | ||
|
|
b800bb0202 | ||
|
|
b41e611cb5 | ||
|
|
ee78c9b08a | ||
|
|
29bb6d76f1 | ||
|
|
38a1c65ab7 | ||
|
|
1ca087b4d0 | ||
|
|
1f21b19eae | ||
|
|
6746fbadef | ||
|
|
41fe8b9494 | ||
|
|
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 |
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
|
||||
@@ -470,7 +473,7 @@ class CastDevice(MediaPlayerEntity):
|
||||
self.hass,
|
||||
refresh_token.id,
|
||||
media_id,
|
||||
timedelta(minutes=5),
|
||||
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
|
||||
)
|
||||
|
||||
# prepend external URL
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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==20210402.1"
|
||||
"home-assistant-frontend==20210407.3"
|
||||
],
|
||||
"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={})
|
||||
|
||||
@@ -148,7 +148,7 @@ class HassIO:
|
||||
|
||||
This method return a coroutine.
|
||||
"""
|
||||
return self.send_command("/discovery", method="get")
|
||||
return self.send_command("/discovery", method="get", timeout=60)
|
||||
|
||||
@api_data
|
||||
def get_discovery_message(self, uuid):
|
||||
|
||||
@@ -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,7 +3,7 @@
|
||||
"name": "Kodi",
|
||||
"documentation": "https://www.home-assistant.io/integrations/kodi",
|
||||
"requirements": [
|
||||
"pykodi==0.2.3"
|
||||
"pykodi==0.2.5"
|
||||
],
|
||||
"codeowners": [
|
||||
"@OnFreund",
|
||||
|
||||
@@ -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,6 +19,8 @@ from . import local_source, models
|
||||
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
|
||||
from .error import Unresolvable
|
||||
|
||||
DEFAULT_EXPIRY_TIME = 3600 * 24
|
||||
|
||||
|
||||
def is_media_source_id(media_content_id: str):
|
||||
"""Test if identifier is a media source."""
|
||||
@@ -105,7 +107,7 @@ async def websocket_browse_media(hass, connection, msg):
|
||||
{
|
||||
vol.Required("type"): "media_source/resolve_media",
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): str,
|
||||
vol.Optional("expires", default=30): int,
|
||||
vol.Optional("expires", default=DEFAULT_EXPIRY_TIME): int,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -116,12 +116,9 @@ class NotifyEventsNotificationService(BaseNotificationService):
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
"""Send a message."""
|
||||
token = self.token
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
token = data.get(ATTR_TOKEN, self.token)
|
||||
|
||||
msg = self.prepare_message(message, data)
|
||||
|
||||
if data.get(ATTR_TOKEN, "").trim():
|
||||
token = data[ATTR_TOKEN]
|
||||
|
||||
msg.send(token)
|
||||
|
||||
@@ -122,7 +122,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get(
|
||||
"feels_like"
|
||||
),
|
||||
ATTR_API_DEW_POINT: (round(current_weather.dewpoint / 100, 1)),
|
||||
ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint),
|
||||
ATTR_API_PRESSURE: current_weather.pressure.get("press"),
|
||||
ATTR_API_HUMIDITY: current_weather.humidity,
|
||||
ATTR_API_WIND_BEARING: current_weather.wind().get("deg"),
|
||||
@@ -178,6 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
return forecast
|
||||
|
||||
@staticmethod
|
||||
def _fmt_dewpoint(dewpoint):
|
||||
if dewpoint is not None:
|
||||
return round(dewpoint / 100, 1)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain(rain):
|
||||
"""Get rain data from weather data."""
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.components.media_player.const import (
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
LIVE_TV_SECTION = "-4"
|
||||
LIVE_TV_SECTION = -4
|
||||
|
||||
|
||||
class PlexSession:
|
||||
|
||||
@@ -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,10 +1,15 @@
|
||||
"""Support for Rituals Perfume Genie switches."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ON_STATE = "1"
|
||||
@@ -33,6 +38,7 @@ class DiffuserSwitch(SwitchEntity):
|
||||
def __init__(self, diffuser):
|
||||
"""Initialize the switch."""
|
||||
self._diffuser = diffuser
|
||||
self._available = True
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
@@ -53,7 +59,7 @@ class DiffuserSwitch(SwitchEntity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the device is available."""
|
||||
return self._diffuser.data["hub"]["status"] == AVAILABLE_STATE
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -89,4 +95,10 @@ class DiffuserSwitch(SwitchEntity):
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the data of the device."""
|
||||
await self._diffuser.update_data()
|
||||
try:
|
||||
await self._diffuser.update_data()
|
||||
except aiohttp.ClientError:
|
||||
self._available = False
|
||||
_LOGGER.error("Unable to retrieve data from rituals.sense-company.com")
|
||||
else:
|
||||
self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,54 +1,112 @@
|
||||
"""The template component."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
from homeassistant.core import CoreState, callback
|
||||
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
|
||||
)
|
||||
|
||||
@@ -65,6 +123,9 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||
|
||||
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],
|
||||
|
||||
@@ -36,7 +36,7 @@ from .const import (
|
||||
)
|
||||
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
||||
|
||||
CONVERSION_PLATFORM = {
|
||||
LEGACY_SENSOR = {
|
||||
CONF_ICON_TEMPLATE: CONF_ICON,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
||||
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
||||
@@ -61,7 +61,7 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
TRIGGER_ENTITY_SCHEMA = vol.Schema(
|
||||
CONFIG_SECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
@@ -71,16 +71,43 @@ TRIGGER_ENTITY_SCHEMA = vol.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]
|
||||
)
|
||||
@@ -88,39 +115,14 @@ async def async_validate_config(hass, config):
|
||||
async_log_exception(err, DOMAIN, cfg, hass)
|
||||
continue
|
||||
|
||||
if CONF_SENSORS not in cfg:
|
||||
trigger_entity_configs.append(cfg)
|
||||
continue
|
||||
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
|
||||
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
|
||||
|
||||
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 CONVERSION_PLATFORM.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)
|
||||
|
||||
cfg = {**cfg, "sensor": sensor}
|
||||
|
||||
trigger_entity_configs.append(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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Sensor that can display the current Home Assistant versions."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource
|
||||
from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
@@ -59,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Version sensor platform."""
|
||||
@@ -114,7 +118,14 @@ class VersionData:
|
||||
@Throttle(TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self):
|
||||
"""Get the latest version information."""
|
||||
await self.api.get_version()
|
||||
try:
|
||||
await self.api.get_version()
|
||||
except HaVersionFetchException as exception:
|
||||
_LOGGER.warning(exception)
|
||||
except HaVersionParseException as exception:
|
||||
_LOGGER.warning(
|
||||
"Could not parse data received for %s - %s", self.api.source, exception
|
||||
)
|
||||
|
||||
|
||||
class VersionSensor(SensorEntity):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"bellows==0.23.1",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.5",
|
||||
"zha-quirks==0.0.55",
|
||||
"zha-quirks==0.0.56",
|
||||
"zigpy-cc==0.5.2",
|
||||
"zigpy-deconz==0.12.0",
|
||||
"zigpy==0.33.0",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 4
|
||||
PATCH_VERSION = "0b4"
|
||||
PATCH_VERSION = "2"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 8, 0)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.42.0
|
||||
home-assistant-frontend==20210402.1
|
||||
home-assistant-frontend==20210407.3
|
||||
httpx==0.17.1
|
||||
jinja2>=2.11.3
|
||||
netdisco==2.8.2
|
||||
|
||||
@@ -763,7 +763,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210402.1
|
||||
home-assistant-frontend==20210407.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -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
|
||||
@@ -1476,7 +1476,7 @@ pykira==0.1.1
|
||||
pykmtronic==0.0.3
|
||||
|
||||
# homeassistant.components.kodi
|
||||
pykodi==0.2.3
|
||||
pykodi==0.2.5
|
||||
|
||||
# homeassistant.components.kulersky
|
||||
pykulersky==0.5.2
|
||||
@@ -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
|
||||
@@ -2372,7 +2372,7 @@ zengge==0.2
|
||||
zeroconf==0.29.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.55
|
||||
zha-quirks==0.0.56
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
@@ -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
|
||||
|
||||
@@ -412,7 +412,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210402.1
|
||||
home-assistant-frontend==20210407.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -518,6 +518,9 @@ netdisco==2.8.2
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.5
|
||||
|
||||
# homeassistant.components.notify_events
|
||||
notify-events==1.0.4
|
||||
|
||||
# homeassistant.components.nsw_fuel_station
|
||||
nsw-fuel-api-client==1.0.10
|
||||
|
||||
@@ -690,7 +693,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
|
||||
@@ -784,7 +787,7 @@ pykira==0.1.1
|
||||
pykmtronic==0.0.3
|
||||
|
||||
# homeassistant.components.kodi
|
||||
pykodi==0.2.3
|
||||
pykodi==0.2.5
|
||||
|
||||
# homeassistant.components.kulersky
|
||||
pykulersky==0.5.2
|
||||
@@ -1095,7 +1098,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
|
||||
@@ -1230,7 +1233,7 @@ zeep[async]==4.0.0
|
||||
zeroconf==0.29.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.55
|
||||
zha-quirks==0.0.56
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-cc==0.5.2
|
||||
@@ -1251,4 +1254,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
1
tests/components/notify_events/__init__.py
Normal file
1
tests/components/notify_events/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the notify_events integration."""
|
||||
12
tests/components/notify_events/test_init.py
Normal file
12
tests/components/notify_events/test_init.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""The tests for notify_events."""
|
||||
from homeassistant.components.notify_events.const import DOMAIN
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_setup(hass):
|
||||
"""Test setup of the integration."""
|
||||
config = {"notify_events": {"token": "ABC"}}
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert DOMAIN in hass.data
|
||||
38
tests/components/notify_events/test_notify.py
Normal file
38
tests/components/notify_events/test_notify.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""The tests for notify_events."""
|
||||
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, DOMAIN
|
||||
from homeassistant.components.notify_events.notify import (
|
||||
ATTR_LEVEL,
|
||||
ATTR_PRIORITY,
|
||||
ATTR_TOKEN,
|
||||
)
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
|
||||
async def test_send_msg(hass):
|
||||
"""Test notify.events service."""
|
||||
notify_calls = async_mock_service(hass, DOMAIN, "events")
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"events",
|
||||
{
|
||||
ATTR_MESSAGE: "message content",
|
||||
ATTR_DATA: {
|
||||
ATTR_TOKEN: "XYZ",
|
||||
ATTR_LEVEL: "warning",
|
||||
ATTR_PRIORITY: "high",
|
||||
},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(notify_calls) == 1
|
||||
call = notify_calls[-1]
|
||||
|
||||
assert call.domain == DOMAIN
|
||||
assert call.service == "events"
|
||||
assert call.data.get(ATTR_MESSAGE) == "message content"
|
||||
assert call.data.get(ATTR_DATA).get(ATTR_TOKEN) == "XYZ"
|
||||
assert call.data.get(ATTR_DATA).get(ATTR_LEVEL) == "warning"
|
||||
assert call.data.get(ATTR_DATA).get(ATTR_PRIORITY) == "high"
|
||||
@@ -55,3 +55,43 @@ async def test_invalid_query(hass):
|
||||
|
||||
state = hass.states.get("sensor.count_tables")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,expected_patterns,not_expected_patterns",
|
||||
[
|
||||
(
|
||||
"sqlite://homeassistant:hunter2@homeassistant.local",
|
||||
["sqlite://****:****@homeassistant.local"],
|
||||
["sqlite://homeassistant:hunter2@homeassistant.local"],
|
||||
),
|
||||
(
|
||||
"sqlite://homeassistant.local",
|
||||
["sqlite://homeassistant.local"],
|
||||
[],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_url(hass, caplog, url, expected_patterns, not_expected_patterns):
|
||||
"""Test credentials in url is not logged."""
|
||||
config = {
|
||||
"sensor": {
|
||||
"platform": "sql",
|
||||
"db_url": url,
|
||||
"queries": [
|
||||
{
|
||||
"name": "count_tables",
|
||||
"query": "SELECT 5 as value",
|
||||
"column": "value",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for pattern in not_expected_patterns:
|
||||
assert pattern not in caplog.text
|
||||
for pattern in expected_patterns:
|
||||
assert pattern in caplog.text
|
||||
|
||||
@@ -266,4 +266,4 @@ async def test_recorder_log(hass, caplog):
|
||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||
await stream.async_record("/example/path")
|
||||
assert "https://abcd:efgh@foo.bar" not in caplog.text
|
||||
assert "https://foo.bar" in caplog.text
|
||||
assert "https://****:****@foo.bar" in caplog.text
|
||||
|
||||
@@ -588,4 +588,4 @@ async def test_worker_log(hass, caplog):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "https://abcd:efgh@foo.bar" not in caplog.text
|
||||
assert "https://foo.bar" in caplog.text
|
||||
assert "https://****:****@foo.bar" in caplog.text
|
||||
|
||||
@@ -27,7 +27,14 @@ async def test_reloadable(hass):
|
||||
"value_template": "{{ states.sensor.test_sensor.state }}"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "event_1"},
|
||||
"sensor": {
|
||||
"name": "top level",
|
||||
"state": "{{ trigger.event.data.source }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -35,8 +42,12 @@ async def test_reloadable(hass):
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire("event_1", {"source": "init"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert hass.states.get("sensor.state").state == "mytest"
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert hass.states.get("sensor.top_level").state == "init"
|
||||
|
||||
yaml_path = path.join(
|
||||
_get_fixtures_base_path(),
|
||||
@@ -52,11 +63,16 @@ async def test_reloadable(hass):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
hass.bus.async_fire("event_2", {"source": "reload"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.state") is None
|
||||
assert hass.states.get("sensor.top_level") is None
|
||||
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
||||
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
|
||||
assert hass.states.get("sensor.top_level_2").state == "reload"
|
||||
|
||||
|
||||
async def test_reloadable_can_remove(hass):
|
||||
@@ -74,7 +90,14 @@ async def test_reloadable_can_remove(hass):
|
||||
"value_template": "{{ states.sensor.test_sensor.state }}"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "event_1"},
|
||||
"sensor": {
|
||||
"name": "top level",
|
||||
"state": "{{ trigger.event.data.source }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -82,8 +105,12 @@ async def test_reloadable_can_remove(hass):
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire("event_1", {"source": "init"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert hass.states.get("sensor.state").state == "mytest"
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert hass.states.get("sensor.top_level").state == "init"
|
||||
|
||||
yaml_path = path.join(
|
||||
_get_fixtures_base_path(),
|
||||
@@ -251,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
assert hass.states.get("sensor.state") is None
|
||||
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
||||
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
|
||||
assert hass.states.get("sensor.top_level_2") is not None
|
||||
|
||||
|
||||
async def test_reload_sensors_that_reference_other_template_sensors(hass):
|
||||
|
||||
@@ -492,6 +492,38 @@ async def test_poll_control_cluster_command(hass, poll_control_device):
|
||||
assert data["device_id"] == poll_control_device.device_id
|
||||
|
||||
|
||||
async def test_poll_control_ignore_list(hass, poll_control_device):
|
||||
"""Test poll control channel ignore list."""
|
||||
set_long_poll_mock = AsyncMock()
|
||||
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
|
||||
cluster = poll_control_ch.cluster
|
||||
|
||||
with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
|
||||
await poll_control_ch.check_in_response(33)
|
||||
|
||||
assert set_long_poll_mock.call_count == 1
|
||||
|
||||
set_long_poll_mock.reset_mock()
|
||||
poll_control_ch.skip_manufacturer_id(4151)
|
||||
with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
|
||||
await poll_control_ch.check_in_response(33)
|
||||
|
||||
assert set_long_poll_mock.call_count == 0
|
||||
|
||||
|
||||
async def test_poll_control_ikea(hass, poll_control_device):
|
||||
"""Test poll control channel ignore list for ikea."""
|
||||
set_long_poll_mock = AsyncMock()
|
||||
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
|
||||
cluster = poll_control_ch.cluster
|
||||
|
||||
poll_control_device.device.node_desc.manufacturer_code = 4476
|
||||
with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
|
||||
await poll_control_ch.check_in_response(33)
|
||||
|
||||
assert set_long_poll_mock.call_count == 0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def zigpy_zll_device(zigpy_device_mock):
|
||||
"""ZLL device fixture."""
|
||||
|
||||
@@ -348,7 +348,9 @@ async def test_thermostat_different_endpoints(
|
||||
"""Test an entity with values on a different endpoint from the primary value."""
|
||||
state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
|
||||
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8
|
||||
assert state.attributes[ATTR_FAN_MODE] == "Auto low"
|
||||
assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
|
||||
|
||||
|
||||
async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration):
|
||||
|
||||
@@ -495,7 +495,7 @@ async def test_poll_value(
|
||||
assert args["valueId"] == {
|
||||
"commandClassName": "Thermostat Mode",
|
||||
"commandClass": 64,
|
||||
"endpoint": 0,
|
||||
"endpoint": 1,
|
||||
"property": "mode",
|
||||
"propertyName": "mode",
|
||||
"metadata": {
|
||||
@@ -503,19 +503,16 @@ async def test_poll_value(
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"min": 0,
|
||||
"max": 31,
|
||||
"max": 255,
|
||||
"label": "Thermostat mode",
|
||||
"states": {
|
||||
"0": "Off",
|
||||
"1": "Heat",
|
||||
"2": "Cool",
|
||||
"3": "Auto",
|
||||
"11": "Energy heat",
|
||||
"12": "Energy cool",
|
||||
},
|
||||
},
|
||||
"value": 1,
|
||||
"ccVersion": 2,
|
||||
"value": 2,
|
||||
"ccVersion": 0,
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
@@ -531,7 +528,7 @@ async def test_poll_value(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 8
|
||||
assert len(client.async_send_command.call_args_list) == 7
|
||||
|
||||
# Test polling against an invalid entity raises ValueError
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@@ -21,3 +21,10 @@ sensor:
|
||||
== "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity")
|
||||
== "Watch Apple TV" %}on{% else %}off{% endif %}'
|
||||
|
||||
template:
|
||||
trigger:
|
||||
platform: event
|
||||
event_type: event_2
|
||||
sensor:
|
||||
name: top level 2
|
||||
state: "{{ trigger.event.data.source }}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2503,4 +2503,7 @@ async def test_undefined_variable(hass, caplog):
|
||||
"""Test a warning is logged on undefined variables."""
|
||||
tpl = template.Template("{{ no_such_variable }}", hass)
|
||||
assert tpl.async_render() == ""
|
||||
assert "Template variable warning: no_such_variable is undefined" in caplog.text
|
||||
assert (
|
||||
"Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user