Compare commits

..

55 Commits

Author SHA1 Message Date
Paulus Schoutsen
7b7cfd71de Merge pull request #48954 from home-assistant/rc 2021-04-09 12:03:40 -07:00
Bram Kragten
d5d9a5ff11 Update frontend to 20210407.3 (#48957) 2021-04-09 18:53:45 +00:00
Paulus Schoutsen
3f744bcbef Bumped version to 2021.4.2 2021-04-09 17:39:03 +00:00
jjlawren
92746aa60c Fix Plex live TV handling (#48953) 2021-04-09 17:38:54 +00:00
Ph-Wagner
4da77b9768 Extend Google Cast media source URL expiry to 24h (#48937)
* Extend media source URL expiry to 12h

closes #46280
After checking out https://github.com/home-assistant/core/pull/48912 I just think why not.

* Update homeassistant/components/cast/media_player.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-04-09 17:38:53 +00:00
David F. Mulcahey
b800bb0202 Bump ZHA quirks library (#48931) 2021-04-09 17:38:52 +00:00
Tobias Sauerwein
b41e611cb5 Bump pykodi to 0.2.5 (#48930) 2021-04-09 17:38:52 +00:00
Philip Allgaier
ee78c9b08a Fix "notify.events" trim() issue + add initial tests (#48928)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-09 17:38:51 +00:00
Joakim Sørensen
29bb6d76f1 Change discovery timeout from 10 to 60 (#48924) 2021-04-09 17:38:50 +00:00
Joakim Sørensen
38a1c65ab7 Handle exceptions when looking for new version (#48922) 2021-04-09 17:38:49 +00:00
Tobias Sauerwein
1ca087b4d0 Bump pykodi to 0.2.4 (#48913) 2021-04-09 17:38:48 +00:00
Erik Montnemery
1f21b19eae Extend media source URL expiry to 24h (#48912) 2021-04-09 17:38:47 +00:00
Milan Meulemans
6746fbadef Catch expected errors and log them in rituals perfume genie (#48870)
* Add update error logging

* Move try available to else

* Remove TimeoutError
2021-04-09 17:38:46 +00:00
Hans Kröner
41fe8b9494 Account for openweathermap 'dew_point' not always being present (#48826) 2021-04-09 17:38:45 +00:00
Paulus Schoutsen
f4c3bdad7d Merge pull request #48896 from home-assistant/rc 2021-04-08 15:35:17 -07:00
Paulus Schoutsen
3bf693e352 Bumped version to 2021.4.1 2021-04-08 21:35:53 +00:00
Bram Kragten
7051cc04bd Update frontend to 20210407.2 (#48888) 2021-04-08 21:35:37 +00:00
Franck Nijhof
d9c1c391bc Fix optional data payload in Prowl messaging service (#48868) 2021-04-08 21:35:35 +00:00
Franck Nijhof
02cd2619bb Fix possibly missing changed_by in Verisure Alarm (#48867) 2021-04-08 21:35:35 +00:00
starkillerOG
f791142c75 Fix motion_blinds gateway signal strength sensor (#48866)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-04-08 21:35:34 +00:00
Bram Kragten
1c939fc9be Correct wrong x in frontend manifest (#48865) 2021-04-08 21:35:33 +00:00
Philip Allgaier
0ad4736349 Bump speedtest-cli to 2.1.3 (#48861) 2021-04-08 21:35:32 +00:00
Erik Montnemery
3f0d63c1ab Validate supported_color_modes for MQTT JSON light (#48836) 2021-04-08 21:35:32 +00:00
Martin Hjelmare
f39afa60ae Fix mysensor cover closed state (#48833) 2021-04-08 21:35:31 +00:00
Erik Montnemery
cf11d9a2df Replace redacted stream recorder credentials with '****' (#48832) 2021-04-08 21:35:30 +00:00
Niccolo Zapponi
dd2a73b363 Fix iCloud extra attributes (#48815) 2021-04-08 21:35:29 +00:00
Johan Nenzén
99ef870908 Add missing super call in Verisure Camera entity (#48812) 2021-04-08 21:35:29 +00:00
Raman Gupta
8d738cff41 Check all endpoints for zwave_js.climate fan mode and operating state (#48800)
* Check all endpoints for zwave_js.climate fan mode and operating state

* fix test
2021-04-08 21:35:28 +00:00
Franck Nijhof
8bdcdfb8e6 Merge pull request #48782 from home-assistant/rc 2021-04-07 19:07:10 +02:00
Franck Nijhof
341531146d Bumped version to 2021.4.0 2021-04-07 18:31:01 +02:00
Bram Kragten
49178d6865 Update frontend to 20210407.1 (#48778) 2021-04-07 18:17:15 +02:00
Erik Montnemery
b4636f17fb Reject nan, inf from generic_thermostat sensor (#48771) 2021-04-07 18:17:11 +02:00
Franck Nijhof
0fb4f31bde Bumped version to 2021.4.0b6 2021-04-07 12:43:04 +02:00
Bram Kragten
b382de96c6 Update frontend to 20210407.0 (#48765) 2021-04-07 12:40:14 +02:00
Erik Montnemery
c9f8861303 Fix whitespace error in cast (#48763) 2021-04-07 12:40:08 +02:00
Erik Montnemery
32511409a9 Remove login details before logging SQL errors (#48758) 2021-04-07 12:40:04 +02:00
Daniel Hjelseth Høyer
e366961ddb Met.no - only update data if coordinates changed (#48756) 2021-04-07 12:40:00 +02:00
J. Nick Koston
bfb8141f55 Solve cast delaying startup when discovered devices are slow to setup (#48755)
* Solve cast delaying startup when devices are slow to setup

* Update homeassistant/components/cast/media_player.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-04-07 12:39:57 +02:00
Joakim Sørensen
537d6412dd Add custom integrations to analytics (#48753) 2021-04-07 12:39:54 +02:00
Stefan Agner
a093cd8ac2 Use microsecond precision for datetime values on MariaDB/MySQL (#48749)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-07 12:39:51 +02:00
Franck Nijhof
322458ee49 Rename hassio config entry title to Supervisor (#48748) 2021-04-07 12:39:48 +02:00
Joakim Sørensen
b573fb49b7 Generate a seperate UUID for the analytics integration (#48742) 2021-04-07 12:39:45 +02:00
Franck Nijhof
15e00b8d18 Do not activate Met.no without setting a Home coordinates (#48741) 2021-04-07 12:39:41 +02:00
Paulus Schoutsen
2db60a3c56 Bumped version to 2021.4.0b5 2021-04-06 19:12:33 +00:00
Paulus Schoutsen
ed90e22421 Updated frontend to 20210406.0 (#48734) 2021-04-06 19:12:28 +00:00
Paulus Schoutsen
d61780dbac Allow reloading top-level template entities (#48733) 2021-04-06 19:12:27 +00:00
Justin Paupore
315e910bfe Fix infinite recursion in LazyState (#48719)
If LazyState cannot parse the attributes of its row as JSON, it prints
a message to the logger. Unfortunately, it passes `self` as a format
argument to that message, which causes its `__repr__` method to be
called, which then tries to retrieve `self.attributes` in order to
display them. This leads to an infinite recursion and a crash of the
entire core.

To fix, send the database row to be printed in the log message, rather
than the LazyState object that wraps around it.
2021-04-06 19:12:26 +00:00
Erik Montnemery
a7523777ba Flag brightness support for MQTT RGB lights (#48718) 2021-04-06 19:12:25 +00:00
Erik Montnemery
7ae65832eb Bump pychromecast to 9.1.2 (#48714) 2021-04-06 19:12:24 +00:00
Erik Montnemery
0df9a8ec38 Improve warnings on undefined template errors (#48713) 2021-04-06 19:12:23 +00:00
J. Nick Koston
5f2a666e76 Abort discovery for unsupported doorbird accessories (#48710) 2021-04-06 19:12:23 +00:00
Paulus Schoutsen
26b9017905 Fix verisure deadlock (#48691) 2021-04-06 19:12:22 +00:00
Raman Gupta
bdd68cd413 Bump zwave_js dependency to 0.23.1 (#48682) 2021-04-06 19:12:21 +00:00
Alexei Chetroi
c512ab7ec9 Implement Ignore list for poll control configuration on Ikea devices (#48667)
Co-authored-by: Hmmbob <33529490+hmmbob@users.noreply.github.com>
2021-04-06 19:12:21 +00:00
mburget
edf41e8425 Fix Raspi GPIO binary_sensor produces unreliable responses (#48170)
* Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable responses ("Doorbell Scenario")

Changes overtaken from PR#31788 which was somehow never finished

* Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable response. Changes taken over from PR31788 which was somehow never finished

* Remove unused code (pylint warning)
2021-04-06 19:12:20 +00:00
78 changed files with 2089 additions and 993 deletions

View File

@@ -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},
)

View File

@@ -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

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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

View File

@@ -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. "

View File

@@ -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()

View File

@@ -62,7 +62,7 @@ MANIFEST_JSON = {
"screenshots": [
{
"src": "/static/images/screenshots/screenshot-1.png",
"sizes": "413×792",
"sizes": "413x792",
"type": "image/png",
}
],

View File

@@ -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",

View File

@@ -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)

View File

@@ -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={})

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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]:

View File

@@ -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]:

View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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)."""

View File

@@ -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}
)

View File

@@ -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 = {

View File

@@ -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"
}
}
}

View File

@@ -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):

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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)

View File

@@ -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}")

View File

@@ -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
)

View File

@@ -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

View File

@@ -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):

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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]

View File

@@ -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],

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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,

View File

@@ -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):

View File

@@ -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()

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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(

View File

@@ -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(

View File

@@ -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):

View File

@@ -0,0 +1 @@
"""Tests for the notify_events integration."""

View 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

View 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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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):

View File

@@ -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 }}"

View File

@@ -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
)