forked from home-assistant/core
Compare commits
92 Commits
2021.4.0b2
...
2021.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
| b5548c57fb | |||
| e5281051a3 | |||
| 346ae78a8e | |||
| b5650bdd52 | |||
| bf28268732 | |||
| 4eb794ae84 | |||
| 21b5551506 | |||
| e0131f726f | |||
| e685b1a1e3 | |||
| 0d00e49dfc | |||
| 36e08e770b | |||
| e3b3d136d8 | |||
| 0a5a5ff053 | |||
| 0bb7592fab | |||
| d081ac8d4a | |||
| b96e0e69f2 | |||
| 82cca8fb1c | |||
| a9602e7a08 | |||
| c08ae64085 | |||
| 01e558430a | |||
| 31b061e8f1 | |||
| 4ca40367d1 | |||
| 12da88cae9 | |||
| 0520ce5ed3 | |||
| 995e22d3bb | |||
| 6296d78e58 | |||
| 2c7fd30029 | |||
| 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 | |||
| 1850b92b36 | |||
| 7b1ea46653 | |||
| a8cd6228cf | |||
| 9eb4397837 | |||
| 311f624adc | |||
| dcb43b474f | |||
| 396a8a3a10 | |||
| 2a1f6d7e8f | |||
| da31328150 | |||
| cec80210a3 |
@@ -283,7 +283,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
temperature_feeling = None
|
||||
town_id = None
|
||||
town_name = None
|
||||
town_timestamp = dt_util.as_utc(elaborated)
|
||||
town_timestamp = dt_util.as_utc(elaborated).isoformat()
|
||||
wind_bearing = None
|
||||
wind_max_speed = None
|
||||
wind_speed = None
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ class CastOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
|
||||
if not bad_cec and not bad_hosts and not bad_uuid:
|
||||
updated_config = {}
|
||||
updated_config = dict(current_config)
|
||||
updated_config[CONF_IGNORE_CEC] = ignore_cec
|
||||
updated_config[CONF_KNOWN_HOSTS] = known_hosts
|
||||
updated_config[CONF_UUID] = wanted_uuid
|
||||
|
||||
@@ -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,11 +1,13 @@
|
||||
"""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
|
||||
import json
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
|
||||
import pychromecast
|
||||
from pychromecast.controllers.homeassistant import HomeAssistantController
|
||||
@@ -185,7 +187,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())
|
||||
)
|
||||
|
||||
@@ -469,8 +473,8 @@ class CastDevice(MediaPlayerEntity):
|
||||
media_id = async_sign_path(
|
||||
self.hass,
|
||||
refresh_token.id,
|
||||
media_id,
|
||||
timedelta(minutes=5),
|
||||
quote(media_id),
|
||||
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
|
||||
)
|
||||
|
||||
# prepend external URL
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "devolo_home_control",
|
||||
"name": "devolo Home Control",
|
||||
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
|
||||
"requirements": ["devolo-home-control-api==0.17.1"],
|
||||
"requirements": ["devolo-home-control-api==0.17.3"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@2Fake", "@Shutgun"],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "DHCP Discovery",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
||||
"requirements": [
|
||||
"scapy==2.4.4", "aiodiscover==1.3.2"
|
||||
"scapy==2.4.4", "aiodiscover==1.3.3"
|
||||
],
|
||||
"codeowners": [
|
||||
"@bdraco"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Support for DoorBird devices."""
|
||||
import asyncio
|
||||
import logging
|
||||
import urllib
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from aiohttp import web
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
@@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
device = DoorBird(device_ip, username, password)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_init_doorbird_device, device)
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == HTTP_UNAUTHORIZED:
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
_LOGGER.error(
|
||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
||||
)
|
||||
@@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
async def _async_register_events(hass, doorstation):
|
||||
try:
|
||||
await hass.async_add_executor_job(doorstation.register_events, hass)
|
||||
except HTTPError:
|
||||
except requests.exceptions.HTTPError:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Doorbird configuration failed. Please verify that API "
|
||||
"Operator permission is enabled for the Doorbird user. "
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Config flow for DoorBird integration."""
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
@@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None):
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
def _check_device(device):
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||
try:
|
||||
status = await hass.async_add_executor_job(device.ready)
|
||||
info = await hass.async_add_executor_job(device.info)
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == HTTP_UNAUTHORIZED:
|
||||
status, info = await hass.async_add_executor_job(_check_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
raise InvalidAuth from err
|
||||
raise CannotConnect from err
|
||||
except OSError as err:
|
||||
@@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
return {"title": data[CONF_HOST], "mac_addr": mac_addr}
|
||||
|
||||
|
||||
async def async_verify_supported_device(hass, host):
|
||||
"""Verify the doorbell state endpoint returns a 401."""
|
||||
device = DoorBird(host, "", "")
|
||||
try:
|
||||
await hass.async_add_executor_job(device.doorbell_state)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTP_UNAUTHORIZED:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for DoorBird."""
|
||||
|
||||
@@ -85,17 +99,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered doorbird device."""
|
||||
macaddress = discovery_info["properties"]["macaddress"]
|
||||
host = discovery_info[CONF_HOST]
|
||||
|
||||
if macaddress[:6] != DOORBIRD_OUI:
|
||||
return self.async_abort(reason="not_doorbird_device")
|
||||
if is_link_local(ip_address(discovery_info[CONF_HOST])):
|
||||
if is_link_local(ip_address(host)):
|
||||
return self.async_abort(reason="link_local_address")
|
||||
if not await async_verify_supported_device(self.hass, host):
|
||||
return self.async_abort(reason="not_doorbird_device")
|
||||
|
||||
await self.async_set_unique_id(macaddress)
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: discovery_info[CONF_HOST]}
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
chop_ending = "._axis-video._tcp.local."
|
||||
friendly_hostname = discovery_info["name"]
|
||||
@@ -104,11 +119,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
CONF_NAME: friendly_hostname,
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_HOST: host,
|
||||
}
|
||||
self.discovery_schema = _schema_with_defaults(
|
||||
host=discovery_info[CONF_HOST], name=friendly_hostname
|
||||
)
|
||||
self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
# Flag to check if the device is connected
|
||||
self._connected = True
|
||||
self._connected_lock = asyncio.Lock()
|
||||
self._zc_lock = asyncio.Lock()
|
||||
self._zc_listening = False
|
||||
# Event the different strategies use for issuing a reconnect attempt.
|
||||
self._reconnect_event = asyncio.Event()
|
||||
# The task containing the infinite reconnect loop while running
|
||||
@@ -270,6 +272,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._entry_data.disconnect_callbacks = []
|
||||
self._entry_data.available = False
|
||||
self._entry_data.async_update_device_state(self._hass)
|
||||
await self._start_zc_listen()
|
||||
|
||||
# Reset tries
|
||||
async with self._tries_lock:
|
||||
@@ -315,6 +318,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._host,
|
||||
error,
|
||||
)
|
||||
await self._start_zc_listen()
|
||||
# Schedule re-connect in event loop in order not to delay HA
|
||||
# startup. First connect is scheduled in tracked tasks.
|
||||
async with self._wait_task_lock:
|
||||
@@ -332,6 +336,7 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
self._tries = 0
|
||||
async with self._connected_lock:
|
||||
self._connected = True
|
||||
await self._stop_zc_listen()
|
||||
self._hass.async_create_task(self._on_login())
|
||||
|
||||
async def _reconnect_once(self):
|
||||
@@ -375,9 +380,6 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
# Create reconnection loop outside of HA's tracked tasks in order
|
||||
# not to delay startup.
|
||||
self._loop_task = self._hass.loop.create_task(self._reconnect_loop())
|
||||
# Listen for mDNS records so we can reconnect directly if a received mDNS record
|
||||
# indicates the node is up again
|
||||
await self._hass.async_add_executor_job(self._zc.add_listener, self, None)
|
||||
|
||||
async with self._connected_lock:
|
||||
self._connected = False
|
||||
@@ -388,11 +390,31 @@ class ReconnectLogic(RecordUpdateListener):
|
||||
if self._loop_task is not None:
|
||||
self._loop_task.cancel()
|
||||
self._loop_task = None
|
||||
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
|
||||
async with self._wait_task_lock:
|
||||
if self._wait_task is not None:
|
||||
self._wait_task.cancel()
|
||||
self._wait_task = None
|
||||
await self._stop_zc_listen()
|
||||
|
||||
async def _start_zc_listen(self):
|
||||
"""Listen for mDNS records.
|
||||
|
||||
This listener allows us to schedule a reconnect as soon as a
|
||||
received mDNS record indicates the node is up again.
|
||||
"""
|
||||
async with self._zc_lock:
|
||||
if not self._zc_listening:
|
||||
await self._hass.async_add_executor_job(
|
||||
self._zc.add_listener, self, None
|
||||
)
|
||||
self._zc_listening = True
|
||||
|
||||
async def _stop_zc_listen(self):
|
||||
"""Stop listening for zeroconf updates."""
|
||||
async with self._zc_lock:
|
||||
if self._zc_listening:
|
||||
await self._hass.async_add_executor_job(self._zc.remove_listener, self)
|
||||
self._zc_listening = False
|
||||
|
||||
@callback
|
||||
def stop_callback(self):
|
||||
|
||||
@@ -62,7 +62,7 @@ MANIFEST_JSON = {
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/images/screenshots/screenshot-1.png",
|
||||
"sizes": "413×792",
|
||||
"sizes": "413x792",
|
||||
"type": "image/png",
|
||||
}
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20210402.0"
|
||||
"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
|
||||
|
||||
|
||||
@@ -209,8 +209,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
}
|
||||
|
||||
if "id" not in properties:
|
||||
_LOGGER.warning(
|
||||
"HomeKit device %s: id not exposed, in violation of spec", properties
|
||||
# This can happen if the TXT record is received after the PTR record
|
||||
# we will wait for the next update in this case
|
||||
_LOGGER.debug(
|
||||
"HomeKit device %s: id not exposed; TXT record may have not yet been received",
|
||||
properties,
|
||||
)
|
||||
return self.async_abort(reason="invalid_properties")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": [
|
||||
"aiohomekit==0.2.60"
|
||||
"aiohomekit==0.2.61"
|
||||
],
|
||||
"zeroconf": [
|
||||
"_hap._tcp.local."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Authentication for HTTP component."""
|
||||
import logging
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.web import middleware
|
||||
@@ -30,11 +31,16 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
|
||||
|
||||
now = dt_util.utcnow()
|
||||
encoded = jwt.encode(
|
||||
{"iss": refresh_token_id, "path": path, "iat": now, "exp": now + expiration},
|
||||
{
|
||||
"iss": refresh_token_id,
|
||||
"path": unquote(path),
|
||||
"iat": now,
|
||||
"exp": now + expiration,
|
||||
},
|
||||
secret,
|
||||
algorithm="HS256",
|
||||
)
|
||||
return f"{path}?{SIGN_QUERY_PARAM}=" f"{encoded.decode()}"
|
||||
return f"{path}?{SIGN_QUERY_PARAM}={encoded.decode()}"
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ def _add_log_filter(logger, patterns):
|
||||
"""Add a Filter to the logger based on a regexp of the filter_str."""
|
||||
|
||||
def filter_func(logrecord):
|
||||
return not any(p.match(logrecord.getMessage()) for p in patterns)
|
||||
return not any(p.search(logrecord.getMessage()) for p in patterns)
|
||||
|
||||
logger.addFilter(filter_func)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from time import gmtime, strftime, time
|
||||
from time import localtime, strftime, time
|
||||
|
||||
from aiolyric.objects.device import LyricDevice
|
||||
from aiolyric.objects.location import LyricLocation
|
||||
@@ -82,7 +82,7 @@ SCHEMA_HOLD_TIME = {
|
||||
vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())),
|
||||
lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,7 +60,7 @@ def setup(hass, config):
|
||||
scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds()
|
||||
|
||||
try:
|
||||
cube = MaxCube(host, port)
|
||||
cube = MaxCube(host, port, now=now)
|
||||
hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval)
|
||||
except timeout as ex:
|
||||
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"domain": "maxcube",
|
||||
"name": "eQ-3 MAX!",
|
||||
"documentation": "https://www.home-assistant.io/integrations/maxcube",
|
||||
"requirements": ["maxcube-api==0.4.1"],
|
||||
"requirements": ["maxcube-api==0.4.2"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -19,6 +20,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 +108,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
|
||||
@@ -121,7 +124,7 @@ async def websocket_resolve_media(hass, connection, msg):
|
||||
url = async_sign_path(
|
||||
hass,
|
||||
connection.refresh_token_id,
|
||||
url,
|
||||
quote(url),
|
||||
timedelta(seconds=msg["expires"]),
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for MQTT fans."""
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -32,6 +33,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
from homeassistant.util.percentage import (
|
||||
int_states_in_range,
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
percentage_to_ranged_value,
|
||||
@@ -224,6 +226,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
self._optimistic_preset_mode = None
|
||||
self._optimistic_speed = None
|
||||
|
||||
self._legacy_speeds_list = []
|
||||
self._legacy_speeds_list_no_off = []
|
||||
|
||||
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
|
||||
|
||||
@staticmethod
|
||||
@@ -284,28 +289,18 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
self._legacy_speeds_list_no_off = speed_list_without_preset_modes(
|
||||
self._legacy_speeds_list
|
||||
)
|
||||
else:
|
||||
self._legacy_speeds_list = []
|
||||
|
||||
self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config
|
||||
self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config
|
||||
if self._feature_preset_mode:
|
||||
self._speeds_list = speed_list_without_preset_modes(
|
||||
self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
|
||||
)
|
||||
self._preset_modes = (
|
||||
self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
|
||||
)
|
||||
self._preset_modes = config[CONF_PRESET_MODES_LIST]
|
||||
else:
|
||||
self._speeds_list = speed_list_without_preset_modes(
|
||||
self._legacy_speeds_list
|
||||
)
|
||||
self._preset_modes = []
|
||||
|
||||
if not self._speeds_list or self._feature_percentage:
|
||||
self._speed_count = 100
|
||||
if self._feature_percentage:
|
||||
self._speed_count = min(int_states_in_range(self._speed_range), 100)
|
||||
else:
|
||||
self._speed_count = len(self._speeds_list)
|
||||
self._speed_count = len(self._legacy_speeds_list_no_off) or 100
|
||||
|
||||
optimistic = config[CONF_OPTIMISTIC]
|
||||
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
|
||||
@@ -327,11 +322,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None
|
||||
and SUPPORT_OSCILLATE
|
||||
)
|
||||
if self._feature_preset_mode and self._speeds_list:
|
||||
self._supported_features |= SUPPORT_SET_SPEED
|
||||
if self._feature_percentage:
|
||||
self._supported_features |= SUPPORT_SET_SPEED
|
||||
if self._feature_legacy_speeds:
|
||||
if self._feature_percentage or self._feature_legacy_speeds:
|
||||
self._supported_features |= SUPPORT_SET_SPEED
|
||||
if self._feature_preset_mode:
|
||||
self._supported_features |= SUPPORT_PRESET_MODE
|
||||
@@ -414,10 +405,6 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
return
|
||||
|
||||
self._preset_mode = preset_mode
|
||||
if not self._implemented_percentage and (preset_mode in self.speed_list):
|
||||
self._percentage = ordered_list_item_to_percentage(
|
||||
self.speed_list, preset_mode
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None:
|
||||
@@ -455,13 +442,12 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
)
|
||||
return
|
||||
|
||||
if not self._implemented_percentage:
|
||||
if speed in self._speeds_list:
|
||||
self._percentage = ordered_list_item_to_percentage(
|
||||
self._speeds_list, speed
|
||||
)
|
||||
elif speed == SPEED_OFF:
|
||||
self._percentage = 0
|
||||
if speed in self._legacy_speeds_list_no_off:
|
||||
self._percentage = ordered_list_item_to_percentage(
|
||||
self._legacy_speeds_list_no_off, speed
|
||||
)
|
||||
elif speed == SPEED_OFF:
|
||||
self._percentage = 0
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -506,19 +492,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def _implemented_percentage(self):
|
||||
"""Return true if percentage has been implemented."""
|
||||
return self._feature_percentage
|
||||
|
||||
@property
|
||||
def _implemented_preset_mode(self):
|
||||
"""Return true if preset_mode has been implemented."""
|
||||
return self._feature_preset_mode
|
||||
|
||||
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||
@property
|
||||
def _implemented_speed(self):
|
||||
def _implemented_speed(self) -> bool:
|
||||
"""Return true if speed has been implemented."""
|
||||
return self._feature_legacy_speeds
|
||||
|
||||
@@ -541,7 +517,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return self._speeds_list
|
||||
return self._legacy_speeds_list_no_off
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
@@ -555,7 +531,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Return the number of speeds the fan supports or 100 if percentage is supported."""
|
||||
"""Return the number of speeds the fan supports."""
|
||||
return self._speed_count
|
||||
|
||||
@property
|
||||
@@ -616,24 +592,12 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
percentage_payload = int(
|
||||
percentage_payload = math.ceil(
|
||||
percentage_to_ranged_value(self._speed_range, percentage)
|
||||
)
|
||||
mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)
|
||||
if self._implemented_preset_mode:
|
||||
if percentage:
|
||||
await self.async_set_preset_mode(
|
||||
preset_mode=percentage_to_ordered_list_item(
|
||||
self.speed_list, percentage
|
||||
)
|
||||
)
|
||||
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||
elif self._feature_legacy_speeds and (
|
||||
SPEED_OFF in self._legacy_speeds_list
|
||||
):
|
||||
await self.async_set_preset_mode(SPEED_OFF)
|
||||
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||
elif self._feature_legacy_speeds:
|
||||
if self._feature_legacy_speeds:
|
||||
if percentage:
|
||||
await self.async_set_speed(
|
||||
percentage_to_ordered_list_item(
|
||||
@@ -644,7 +608,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
elif SPEED_OFF in self._legacy_speeds_list:
|
||||
await self.async_set_speed(SPEED_OFF)
|
||||
|
||||
if self._implemented_percentage:
|
||||
if self._feature_percentage:
|
||||
mqtt.async_publish(
|
||||
self.hass,
|
||||
self._topic[CONF_PERCENTAGE_COMMAND_TOPIC],
|
||||
@@ -665,13 +629,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
if preset_mode not in self.preset_modes:
|
||||
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
|
||||
return
|
||||
# Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||
if preset_mode in self._legacy_speeds_list:
|
||||
await self.async_set_speed(speed=preset_mode)
|
||||
if not self._implemented_percentage and preset_mode in self.speed_list:
|
||||
self._percentage = ordered_list_item_to_percentage(
|
||||
self.speed_list, preset_mode
|
||||
)
|
||||
|
||||
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
|
||||
|
||||
mqtt.async_publish(
|
||||
@@ -693,18 +651,18 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
speed_payload = None
|
||||
if self._feature_legacy_speeds:
|
||||
if speed in self._legacy_speeds_list:
|
||||
if speed == SPEED_LOW:
|
||||
speed_payload = self._payload["SPEED_LOW"]
|
||||
elif speed == SPEED_MEDIUM:
|
||||
speed_payload = self._payload["SPEED_MEDIUM"]
|
||||
elif speed == SPEED_HIGH:
|
||||
speed_payload = self._payload["SPEED_HIGH"]
|
||||
elif speed == SPEED_OFF:
|
||||
speed_payload = self._payload["SPEED_OFF"]
|
||||
else:
|
||||
_LOGGER.warning("'%s'is not a valid speed", speed)
|
||||
return
|
||||
speed_payload = self._payload["SPEED_OFF"]
|
||||
else:
|
||||
_LOGGER.warning("'%s' is not a valid speed", speed)
|
||||
return
|
||||
|
||||
if speed_payload:
|
||||
mqtt.async_publish(
|
||||
|
||||
@@ -35,6 +35,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_WHITE_VALUE,
|
||||
VALID_COLOR_MODES,
|
||||
LightEntity,
|
||||
valid_supported_color_modes,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
@@ -130,7 +131,10 @@ PLATFORM_SCHEMA_JSON = vol.All(
|
||||
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All(
|
||||
cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique()
|
||||
cv.ensure_list,
|
||||
[vol.In(VALID_COLOR_MODES)],
|
||||
vol.Unique(),
|
||||
valid_supported_color_modes,
|
||||
),
|
||||
vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean,
|
||||
vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
|
||||
@@ -197,7 +201,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS
|
||||
self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP
|
||||
self._supported_features |= config[CONF_HS] and SUPPORT_COLOR
|
||||
self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR
|
||||
self._supported_features |= config[CONF_RGB] and (
|
||||
SUPPORT_COLOR | SUPPORT_BRIGHTNESS
|
||||
)
|
||||
self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE
|
||||
self._supported_features |= config[CONF_XY] and SUPPORT_COLOR
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
and self._templates[CONF_GREEN_TEMPLATE] is not None
|
||||
and self._templates[CONF_BLUE_TEMPLATE] is not None
|
||||
):
|
||||
features = features | SUPPORT_COLOR
|
||||
features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS
|
||||
if self._config.get(CONF_EFFECT_LIST) is not None:
|
||||
features = features | SUPPORT_EFFECT
|
||||
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:
|
||||
|
||||
@@ -70,12 +70,12 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
|
||||
else:
|
||||
amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
|
||||
|
||||
if amount == 0:
|
||||
return CoverState.CLOSED
|
||||
if v_up and not v_down and not v_stop:
|
||||
return CoverState.OPENING
|
||||
if not v_up and v_down and not v_stop:
|
||||
return CoverState.CLOSING
|
||||
if not v_up and not v_down and v_stop and amount == 0:
|
||||
return CoverState.CLOSED
|
||||
return CoverState.OPEN
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "nexia",
|
||||
"name": "Nexia",
|
||||
"requirements": ["nexia==0.9.5"],
|
||||
"requirements": ["nexia==0.9.6"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -13,4 +13,6 @@ def is_invalid_auth_code(http_status_code):
|
||||
|
||||
def percent_conv(val):
|
||||
"""Convert an actual percentage (0.0-1.0) to 0-100 scale."""
|
||||
if val is None:
|
||||
return None
|
||||
return round(val * 100.0, 1)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -134,8 +134,12 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
async def _notify_task(self):
|
||||
while self.api.on and self.api.notify_change_supported:
|
||||
if await self.api.notifyChange(130):
|
||||
res = await self.api.notifyChange(130)
|
||||
if res:
|
||||
self.async_set_updated_data(None)
|
||||
elif res is None:
|
||||
LOGGER.debug("Aborting notify due to unexpected return")
|
||||
break
|
||||
|
||||
@callback
|
||||
def _async_notify_stop(self):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Philips TV",
|
||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||
"requirements": [
|
||||
"ha-philipsjs==2.3.2"
|
||||
"ha-philipsjs==2.7.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@elupus"
|
||||
|
||||
@@ -24,20 +24,22 @@ async def async_setup(hass, config):
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_next_ping_id(hass):
|
||||
def async_get_next_ping_id(hass, count=1):
|
||||
"""Find the next id to use in the outbound ping.
|
||||
|
||||
When using multiping, we increment the id
|
||||
by the number of ids that multiping
|
||||
will use.
|
||||
|
||||
Must be called in async
|
||||
"""
|
||||
current_id = hass.data[DOMAIN][PING_ID]
|
||||
if current_id == MAX_PING_ID:
|
||||
next_id = DEFAULT_START_ID
|
||||
else:
|
||||
next_id = current_id + 1
|
||||
|
||||
hass.data[DOMAIN][PING_ID] = next_id
|
||||
|
||||
return next_id
|
||||
allocated_id = hass.data[DOMAIN][PING_ID] + 1
|
||||
if allocated_id > MAX_PING_ID:
|
||||
allocated_id -= MAX_PING_ID - DEFAULT_START_ID
|
||||
hass.data[DOMAIN][PING_ID] += count
|
||||
if hass.data[DOMAIN][PING_ID] > MAX_PING_ID:
|
||||
hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID
|
||||
return allocated_id
|
||||
|
||||
|
||||
def _can_use_icmp_lib_with_privilege() -> None | bool:
|
||||
|
||||
@@ -125,7 +125,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
count=PING_ATTEMPTS_COUNT,
|
||||
timeout=ICMP_TIMEOUT,
|
||||
privileged=privileged,
|
||||
id=async_get_next_ping_id(hass),
|
||||
id=async_get_next_ping_id(hass, len(ip_to_dev_id)),
|
||||
)
|
||||
)
|
||||
_LOGGER.debug("Multiping responses: %s", responses)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,10 +47,12 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Set up Roku from a config entry."""
|
||||
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinator = hass.data[DOMAIN].get(entry.entry_id)
|
||||
if not coordinator:
|
||||
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -195,9 +195,15 @@ class ScreenlogicEntity(CoordinatorEntity):
|
||||
"""Return device information for the controller."""
|
||||
controller_type = self.config_data["controller_type"]
|
||||
hardware_type = self.config_data["hardware_type"]
|
||||
try:
|
||||
equipment_model = EQUIPMENT.CONTROLLER_HARDWARE[controller_type][
|
||||
hardware_type
|
||||
]
|
||||
except KeyError:
|
||||
equipment_model = f"Unknown Model C:{controller_type} H:{hardware_type}"
|
||||
return {
|
||||
"connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)},
|
||||
"name": self.gateway_name,
|
||||
"manufacturer": "Pentair",
|
||||
"model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type],
|
||||
"model": equipment_model,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
EVENT_SHELLY_CLICK,
|
||||
INPUTS_EVENTS_SUBTYPES,
|
||||
SHBTN_1_INPUTS_EVENTS_TYPES,
|
||||
SUPPORTED_INPUTS_EVENTS_TYPES,
|
||||
)
|
||||
from .utils import get_device_wrapper, get_input_triggers
|
||||
@@ -45,7 +46,7 @@ async def async_validate_trigger_config(hass, config):
|
||||
|
||||
# if device is available verify parameters against device capabilities
|
||||
wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID])
|
||||
if not wrapper:
|
||||
if not wrapper or not wrapper.device.initialized:
|
||||
return config
|
||||
|
||||
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||
@@ -68,6 +69,19 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
|
||||
if not wrapper:
|
||||
raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}")
|
||||
|
||||
if wrapper.model in ("SHBTN-1", "SHBTN-2"):
|
||||
for trigger in SHBTN_1_INPUTS_EVENTS_TYPES:
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_TYPE: trigger,
|
||||
CONF_SUBTYPE: "button",
|
||||
}
|
||||
)
|
||||
return triggers
|
||||
|
||||
for block in wrapper.device.blocks:
|
||||
input_triggers = get_input_triggers(wrapper.device, block)
|
||||
|
||||
|
||||
@@ -118,15 +118,16 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
|
||||
"""Brightness of light."""
|
||||
if self.mode == "color":
|
||||
if self.control_result:
|
||||
brightness = self.control_result["gain"]
|
||||
brightness_pct = self.control_result["gain"]
|
||||
else:
|
||||
brightness = self.block.gain
|
||||
brightness_pct = self.block.gain
|
||||
else:
|
||||
if self.control_result:
|
||||
brightness = self.control_result["brightness"]
|
||||
brightness_pct = self.control_result["brightness"]
|
||||
else:
|
||||
brightness = self.block.brightness
|
||||
return int(brightness / 100 * 255)
|
||||
brightness_pct = self.block.brightness
|
||||
|
||||
return round(255 * brightness_pct / 100)
|
||||
|
||||
@property
|
||||
def white_value(self) -> int:
|
||||
@@ -188,11 +189,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
|
||||
set_mode = None
|
||||
params = {"turn": "on"}
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100)
|
||||
brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255)
|
||||
if hasattr(self.block, "gain"):
|
||||
params["gain"] = tmp_brightness
|
||||
params["gain"] = brightness_pct
|
||||
if hasattr(self.block, "brightness"):
|
||||
params["brightness"] = tmp_brightness
|
||||
params["brightness"] = brightness_pct
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
|
||||
color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==0.6.1"],
|
||||
"requirements": ["aioshelly==0.6.2"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
|
||||
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"dependencies": [],
|
||||
"codeowners": ["@mdz"],
|
||||
"requirements": [
|
||||
"python-smarttub==0.0.19"
|
||||
"python-smarttub==0.0.23"
|
||||
],
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"name": "Speedtest.net",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/speedtestdotnet",
|
||||
"requirements": ["speedtest-cli==2.1.2"],
|
||||
"requirements": [
|
||||
"speedtest-cli==2.1.3"
|
||||
],
|
||||
"codeowners": ["@rohankapoorcom", "@engrbm87"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import logging
|
||||
import re
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
@@ -18,6 +19,13 @@ CONF_COLUMN_NAME = "column"
|
||||
CONF_QUERIES = "queries"
|
||||
CONF_QUERY = "query"
|
||||
|
||||
DB_URL_RE = re.compile("//.*:.*@")
|
||||
|
||||
|
||||
def redact_credentials(data):
|
||||
"""Redact credentials from string data."""
|
||||
return DB_URL_RE.sub("//****:****@", data)
|
||||
|
||||
|
||||
def validate_sql_select(value):
|
||||
"""Validate that value is a SQL SELECT query."""
|
||||
@@ -47,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
if not db_url:
|
||||
db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE))
|
||||
|
||||
sess = None
|
||||
try:
|
||||
engine = sqlalchemy.create_engine(db_url)
|
||||
sessmaker = scoped_session(sessionmaker(bind=engine))
|
||||
@@ -56,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
sess.execute("SELECT 1;")
|
||||
|
||||
except sqlalchemy.exc.SQLAlchemyError as err:
|
||||
_LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
|
||||
_LOGGER.error(
|
||||
"Couldn't connect using %s DB_URL: %s",
|
||||
redact_credentials(db_url),
|
||||
redact_credentials(str(err)),
|
||||
)
|
||||
return
|
||||
finally:
|
||||
sess.close()
|
||||
if sess:
|
||||
sess.close()
|
||||
|
||||
queries = []
|
||||
|
||||
@@ -147,7 +161,11 @@ class SQLSensor(SensorEntity):
|
||||
value = str(value)
|
||||
self._attributes[key] = value
|
||||
except sqlalchemy.exc.SQLAlchemyError as err:
|
||||
_LOGGER.error("Error executing query %s: %s", self._query, err)
|
||||
_LOGGER.error(
|
||||
"Error executing query %s: %s",
|
||||
self._query,
|
||||
redact_credentials(str(err)),
|
||||
)
|
||||
return
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
@@ -39,7 +39,12 @@ from .hls import async_setup_hls
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STREAM_SOURCE_RE = re.compile("//(.*):(.*)@")
|
||||
STREAM_SOURCE_RE = re.compile("//.*:.*@")
|
||||
|
||||
|
||||
def redact_credentials(data):
|
||||
"""Redact credentials from string data."""
|
||||
return STREAM_SOURCE_RE.sub("//****:****@", data)
|
||||
|
||||
|
||||
def create_stream(hass, stream_source, options=None):
|
||||
@@ -176,9 +181,7 @@ class Stream:
|
||||
target=self._run_worker,
|
||||
)
|
||||
self._thread.start()
|
||||
_LOGGER.info(
|
||||
"Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
|
||||
)
|
||||
_LOGGER.info("Started stream: %s", redact_credentials(str(self.source)))
|
||||
|
||||
def update_source(self, new_source):
|
||||
"""Restart the stream with a new stream source."""
|
||||
@@ -244,9 +247,7 @@ class Stream:
|
||||
self._thread_quit.set()
|
||||
self._thread.join()
|
||||
self._thread = None
|
||||
_LOGGER.info(
|
||||
"Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source))
|
||||
)
|
||||
_LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source)))
|
||||
|
||||
async def async_record(self, video_path, duration=30, lookback=5):
|
||||
"""Make a .mp4 recording from a provided stream."""
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
|
||||
import av
|
||||
|
||||
from . import STREAM_SOURCE_RE
|
||||
from . import redact_credentials
|
||||
from .const import (
|
||||
AUDIO_CODECS,
|
||||
MAX_MISSING_DTS,
|
||||
@@ -128,9 +128,7 @@ def stream_worker(source, options, segment_buffer, quit_event):
|
||||
try:
|
||||
container = av.open(source, options=options, timeout=STREAM_TIMEOUT)
|
||||
except av.AVError:
|
||||
_LOGGER.error(
|
||||
"Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source))
|
||||
)
|
||||
_LOGGER.error("Error opening stream %s", redact_credentials(str(source)))
|
||||
return
|
||||
try:
|
||||
video_stream = container.streams.video[0]
|
||||
|
||||
@@ -1,68 +1,131 @@
|
||||
"""The template component."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import CoreState, callback
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
|
||||
from homeassistant.core import CoreState, Event, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
discovery,
|
||||
trigger as trigger_helper,
|
||||
update_coordinator,
|
||||
)
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.reload import async_reload_integration_platforms
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the template integration."""
|
||||
if DOMAIN in config:
|
||||
for conf in config[DOMAIN]:
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(config)
|
||||
await _process_config(hass, config)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
async def _reload_config(call: Event) -> None:
|
||||
"""Reload top-level + platforms."""
|
||||
try:
|
||||
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf = await conf_util.async_process_component_config(
|
||||
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
|
||||
)
|
||||
|
||||
if conf is None:
|
||||
return
|
||||
|
||||
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
if DOMAIN in conf:
|
||||
await _process_config(hass, conf)
|
||||
|
||||
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
|
||||
|
||||
hass.helpers.service.async_register_admin_service(
|
||||
DOMAIN, SERVICE_RELOAD, _reload_config
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _process_config(hass, config):
|
||||
"""Process config."""
|
||||
coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN)
|
||||
|
||||
# Remove old ones
|
||||
if coordinators:
|
||||
for coordinator in coordinators:
|
||||
coordinator.async_remove()
|
||||
|
||||
async def init_coordinator(hass, conf):
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(config)
|
||||
return coordinator
|
||||
|
||||
hass.data[DOMAIN] = await asyncio.gather(
|
||||
*[init_coordinator(hass, conf) for conf in config[DOMAIN]]
|
||||
)
|
||||
|
||||
|
||||
class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||
"""Class to handle incoming data."""
|
||||
|
||||
REMOVE_TRIGGER = object()
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Instantiate trigger data."""
|
||||
super().__init__(
|
||||
hass, logging.getLogger(__name__), name="Trigger Update Coordinator"
|
||||
)
|
||||
super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
|
||||
self.config = config
|
||||
self._unsub_trigger = None
|
||||
self._unsub_start: Callable[[], None] | None = None
|
||||
self._unsub_trigger: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return unique ID for the entity."""
|
||||
return self.config.get("unique_id")
|
||||
|
||||
@callback
|
||||
def async_remove(self):
|
||||
"""Signal that the entities need to remove themselves."""
|
||||
if self._unsub_start:
|
||||
self._unsub_start()
|
||||
if self._unsub_trigger:
|
||||
self._unsub_trigger()
|
||||
|
||||
async def async_setup(self, hass_config):
|
||||
"""Set up the trigger and create entities."""
|
||||
if self.hass.state == CoreState.running:
|
||||
await self._attach_triggers()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
self._unsub_start = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, self._attach_triggers
|
||||
)
|
||||
|
||||
self.hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
self.hass,
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
{"coordinator": self, "entities": self.config[CONF_SENSORS]},
|
||||
hass_config,
|
||||
for platform_domain in (SENSOR_DOMAIN,):
|
||||
self.hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
self.hass,
|
||||
platform_domain,
|
||||
DOMAIN,
|
||||
{"coordinator": self, "entities": self.config[platform_domain]},
|
||||
hass_config,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _attach_triggers(self, start_event=None) -> None:
|
||||
"""Attach the triggers."""
|
||||
if start_event is not None:
|
||||
self._unsub_start = None
|
||||
|
||||
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
|
||||
self.hass,
|
||||
self.config[CONF_TRIGGER],
|
||||
|
||||
@@ -1,49 +1,128 @@
|
||||
"""Template config validator."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
)
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_SENSORS,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.trigger import async_validate_trigger_config
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN
|
||||
from .sensor import SENSOR_SCHEMA
|
||||
from .const import (
|
||||
CONF_ATTRIBUTE_TEMPLATES,
|
||||
CONF_ATTRIBUTES,
|
||||
CONF_AVAILABILITY,
|
||||
CONF_AVAILABILITY_TEMPLATE,
|
||||
CONF_PICTURE,
|
||||
CONF_TRIGGER,
|
||||
DOMAIN,
|
||||
)
|
||||
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
||||
|
||||
CONF_STATE = "state"
|
||||
LEGACY_SENSOR = {
|
||||
CONF_ICON_TEMPLATE: CONF_ICON,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
||||
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
||||
CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
|
||||
CONF_FRIENDLY_NAME: CONF_NAME,
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
|
||||
TRIGGER_ENTITY_SCHEMA = vol.Schema(
|
||||
SENSOR_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_AVAILABILITY): cv.template,
|
||||
vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
|
||||
vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
|
||||
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _rewrite_legacy_to_modern_trigger_conf(cfg: dict):
|
||||
"""Rewrite a legacy to a modern trigger-basd conf."""
|
||||
logging.getLogger(__name__).warning(
|
||||
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
|
||||
)
|
||||
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
|
||||
|
||||
for device_id, entity_cfg in cfg[CONF_SENSORS].items():
|
||||
entity_cfg = {**entity_cfg}
|
||||
|
||||
for from_key, to_key in LEGACY_SENSOR.items():
|
||||
if from_key not in entity_cfg or to_key in entity_cfg:
|
||||
continue
|
||||
|
||||
val = entity_cfg.pop(from_key)
|
||||
if isinstance(val, str):
|
||||
val = template.Template(val)
|
||||
entity_cfg[to_key] = val
|
||||
|
||||
if CONF_NAME not in entity_cfg:
|
||||
entity_cfg[CONF_NAME] = template.Template(device_id)
|
||||
|
||||
sensor.append(entity_cfg)
|
||||
|
||||
return {**cfg, "sensor": sensor}
|
||||
|
||||
|
||||
async def async_validate_config(hass, config):
|
||||
"""Validate config."""
|
||||
if DOMAIN not in config:
|
||||
return config
|
||||
|
||||
trigger_entity_configs = []
|
||||
config_sections = []
|
||||
|
||||
for cfg in cv.ensure_list(config[DOMAIN]):
|
||||
try:
|
||||
cfg = TRIGGER_ENTITY_SCHEMA(cfg)
|
||||
cfg = CONFIG_SECTION_SCHEMA(cfg)
|
||||
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
hass, cfg[CONF_TRIGGER]
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
async_log_exception(err, DOMAIN, cfg, hass)
|
||||
continue
|
||||
|
||||
else:
|
||||
trigger_entity_configs.append(cfg)
|
||||
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
|
||||
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
|
||||
|
||||
config_sections.append(cfg)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
config = config_without_domain(config, DOMAIN)
|
||||
config[DOMAIN] = trigger_entity_configs
|
||||
config[DOMAIN] = config_sections
|
||||
|
||||
return config
|
||||
|
||||
@@ -20,3 +20,7 @@ PLATFORMS = [
|
||||
"vacuum",
|
||||
"weather",
|
||||
]
|
||||
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_PICTURE = "picture"
|
||||
|
||||
@@ -5,6 +5,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
@@ -17,6 +18,7 @@ from homeassistant.const import (
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_SENSORS,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -88,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config):
|
||||
friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
|
||||
unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
|
||||
attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
|
||||
unique_id = device_config.get(CONF_UNIQUE_ID)
|
||||
|
||||
sensors.append(
|
||||
@@ -117,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
async_add_entities(_async_create_template_tracking_entities(hass, config))
|
||||
else:
|
||||
async_add_entities(
|
||||
TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config)
|
||||
for device_id, config in discovery_info["entities"].items()
|
||||
TriggerSensorEntity(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
)
|
||||
|
||||
|
||||
@@ -201,9 +203,10 @@ class SensorTemplate(TemplateEntity, SensorEntity):
|
||||
class TriggerSensorEntity(TriggerEntity, SensorEntity):
|
||||
"""Sensor entity based on trigger data."""
|
||||
|
||||
extra_template_keys = (CONF_VALUE_TEMPLATE,)
|
||||
domain = SENSOR_DOMAIN
|
||||
extra_template_keys = (CONF_STATE,)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return state of the sensor."""
|
||||
return self._rendered.get(CONF_VALUE_TEMPLATE)
|
||||
return self._rendered.get(CONF_STATE)
|
||||
|
||||
@@ -6,20 +6,16 @@ from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import template, update_coordinator
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
|
||||
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
|
||||
|
||||
|
||||
class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
@@ -32,23 +28,13 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
device_id: str,
|
||||
config: dict,
|
||||
):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_id = async_generate_entity_id(
|
||||
self.domain + ".{}", device_id, hass=hass
|
||||
)
|
||||
|
||||
self._name = config.get(CONF_FRIENDLY_NAME, device_id)
|
||||
|
||||
entity_unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
if entity_unique_id is None and coordinator.unique_id:
|
||||
entity_unique_id = device_id
|
||||
|
||||
if entity_unique_id and coordinator.unique_id:
|
||||
self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}"
|
||||
else:
|
||||
@@ -56,32 +42,33 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
|
||||
self._config = config
|
||||
|
||||
self._to_render = [
|
||||
itm
|
||||
for itm in (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_AVAILABILITY_TEMPLATE,
|
||||
)
|
||||
if itm in config
|
||||
]
|
||||
self._static_rendered = {}
|
||||
self._to_render = []
|
||||
|
||||
for itm in (
|
||||
CONF_NAME,
|
||||
CONF_ICON,
|
||||
CONF_PICTURE,
|
||||
CONF_AVAILABILITY,
|
||||
):
|
||||
if itm not in config:
|
||||
continue
|
||||
|
||||
if config[itm].is_static:
|
||||
self._static_rendered[itm] = config[itm].template
|
||||
else:
|
||||
self._to_render.append(itm)
|
||||
|
||||
if self.extra_template_keys is not None:
|
||||
self._to_render.extend(self.extra_template_keys)
|
||||
|
||||
self._rendered = {}
|
||||
# We make a copy so our initial render is 'unknown' and not 'unavailable'
|
||||
self._rendered = dict(self._static_rendered)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the entity."""
|
||||
if (
|
||||
self._rendered is not None
|
||||
and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None
|
||||
):
|
||||
return name
|
||||
return self._name
|
||||
return self._rendered.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -101,29 +88,27 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return icon."""
|
||||
return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE)
|
||||
return self._rendered.get(CONF_ICON)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return entity picture."""
|
||||
return self._rendered is not None and self._rendered.get(
|
||||
CONF_ENTITY_PICTURE_TEMPLATE
|
||||
)
|
||||
return self._rendered.get(CONF_PICTURE)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return availability of the entity."""
|
||||
return (
|
||||
self._rendered is not None
|
||||
self._rendered is not self._static_rendered
|
||||
and
|
||||
# Check against False so `None` is ok
|
||||
self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False
|
||||
self._rendered.get(CONF_AVAILABILITY) is not False
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return extra attributes."""
|
||||
return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES)
|
||||
return self._rendered.get(CONF_ATTRIBUTES)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle being added to Home Assistant."""
|
||||
@@ -136,16 +121,16 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
rendered = {}
|
||||
rendered = dict(self._static_rendered)
|
||||
|
||||
for key in self._to_render:
|
||||
rendered[key] = self._config[key].async_render(
|
||||
self.coordinator.data["run_variables"], parse_result=False
|
||||
)
|
||||
|
||||
if CONF_ATTRIBUTE_TEMPLATES in self._config:
|
||||
rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex(
|
||||
self._config[CONF_ATTRIBUTE_TEMPLATES],
|
||||
if CONF_ATTRIBUTES in self._config:
|
||||
rendered[CONF_ATTRIBUTES] = template.render_complex(
|
||||
self._config[CONF_ATTRIBUTES],
|
||||
self.coordinator.data["run_variables"],
|
||||
)
|
||||
|
||||
@@ -154,7 +139,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):
|
||||
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
|
||||
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
||||
)
|
||||
self._rendered = None
|
||||
self._rendered = self._static_rendered
|
||||
|
||||
self.async_set_context(self.coordinator.data["context"])
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
HTTP_BAD_REQUEST,
|
||||
HTTP_NOT_FOUND,
|
||||
PLATFORM_FORMAT,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -316,6 +317,10 @@ class SpeechManager:
|
||||
provider.name = engine
|
||||
self.providers[engine] = provider
|
||||
|
||||
self.hass.config.components.add(
|
||||
PLATFORM_FORMAT.format(domain=engine, platform=DOMAIN)
|
||||
)
|
||||
|
||||
async def async_get_url_path(
|
||||
self, engine, message, cache=None, language=None, options=None
|
||||
):
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"requirements": ["mutagen==1.45.1"],
|
||||
"dependencies": ["http"],
|
||||
"after_dependencies": ["media_player"],
|
||||
"quality_scale": "internal",
|
||||
"codeowners": ["@pvizeli"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -150,6 +150,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
command_class=CommandClass.THERMOSTAT_OPERATING_STATE,
|
||||
add_to_watched_value_ids=True,
|
||||
check_all_endpoints=True,
|
||||
)
|
||||
self._current_temp = self.get_zwave_value(
|
||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||
@@ -169,11 +170,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
THERMOSTAT_MODE_PROPERTY,
|
||||
CommandClass.THERMOSTAT_FAN_MODE,
|
||||
add_to_watched_value_ids=True,
|
||||
check_all_endpoints=True,
|
||||
)
|
||||
self._fan_state = self.get_zwave_value(
|
||||
THERMOSTAT_OPERATING_STATE_PROPERTY,
|
||||
CommandClass.THERMOSTAT_FAN_STATE,
|
||||
add_to_watched_value_ids=True,
|
||||
check_all_endpoints=True,
|
||||
)
|
||||
self._set_modes_and_presets()
|
||||
self._supported_features = 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.23.0"],
|
||||
"requirements": ["zwave-js-server-python==0.23.1"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["http", "websocket_api"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import weakref
|
||||
import attr
|
||||
|
||||
from homeassistant import data_entry_flow, loader
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
from homeassistant.helpers.event import Event
|
||||
@@ -276,14 +277,19 @@ class ConfigEntry:
|
||||
wait_time,
|
||||
)
|
||||
|
||||
async def setup_again(now: Any) -> None:
|
||||
async def setup_again(*_: Any) -> None:
|
||||
"""Run setup again."""
|
||||
self._async_cancel_retry_setup = None
|
||||
await self.async_setup(hass, integration=integration, tries=tries)
|
||||
|
||||
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
|
||||
wait_time, setup_again
|
||||
)
|
||||
if hass.state == CoreState.running:
|
||||
self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
|
||||
wait_time, setup_again
|
||||
)
|
||||
else:
|
||||
self._async_cancel_retry_setup = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, setup_again
|
||||
)
|
||||
return
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 4
|
||||
PATCH_VERSION = "0b2"
|
||||
PATCH_VERSION = "4"
|
||||
__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
|
||||
@@ -336,7 +339,7 @@ class Template:
|
||||
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
|
||||
"""
|
||||
if self.is_static:
|
||||
if self.hass.config.legacy_templates or not parse_result:
|
||||
if not parse_result or self.hass.config.legacy_templates:
|
||||
return self.template
|
||||
return self._parse_result(self.template)
|
||||
|
||||
@@ -360,7 +363,7 @@ class Template:
|
||||
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
|
||||
"""
|
||||
if self.is_static:
|
||||
if self.hass.config.legacy_templates or not parse_result:
|
||||
if not parse_result or self.hass.config.legacy_templates:
|
||||
return self.template
|
||||
return self._parse_result(self.template)
|
||||
|
||||
@@ -370,7 +373,7 @@ class Template:
|
||||
kwargs.update(variables)
|
||||
|
||||
try:
|
||||
render_result = compiled.render(kwargs)
|
||||
render_result = _render_with_context(self.template, compiled, **kwargs)
|
||||
except Exception as err:
|
||||
raise TemplateError(err) from err
|
||||
|
||||
@@ -442,7 +445,7 @@ class Template:
|
||||
|
||||
def _render_template() -> None:
|
||||
try:
|
||||
compiled.render(kwargs)
|
||||
_render_with_context(self.template, compiled, **kwargs)
|
||||
except TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
@@ -524,7 +527,9 @@ class Template:
|
||||
variables["value_json"] = json.loads(value)
|
||||
|
||||
try:
|
||||
return self._compiled.render(variables).strip()
|
||||
return _render_with_context(
|
||||
self.template, self._compiled, **variables
|
||||
).strip()
|
||||
except jinja2.TemplateError as ex:
|
||||
if error_value is _SENTINEL:
|
||||
_LOGGER.error(
|
||||
@@ -535,7 +540,7 @@ class Template:
|
||||
)
|
||||
return value if error_value is _SENTINEL else error_value
|
||||
|
||||
def _ensure_compiled(self, limited: bool = False) -> Template:
|
||||
def _ensure_compiled(self, limited: bool = False) -> jinja2.Template:
|
||||
"""Bind a template to a specific hass instance."""
|
||||
self.ensure_valid()
|
||||
|
||||
@@ -548,7 +553,7 @@ class Template:
|
||||
env = self._env
|
||||
|
||||
self._compiled = cast(
|
||||
Template,
|
||||
jinja2.Template,
|
||||
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
|
||||
)
|
||||
|
||||
@@ -1314,12 +1319,59 @@ def urlencode(value):
|
||||
return urllib_urlencode(value).encode("utf-8")
|
||||
|
||||
|
||||
def _render_with_context(
|
||||
template_str: str, template: jinja2.Template, **kwargs: Any
|
||||
) -> str:
|
||||
"""Store template being rendered in a ContextVar to aid error handling."""
|
||||
template_cv.set(template_str)
|
||||
return template.render(**kwargs)
|
||||
|
||||
|
||||
class LoggingUndefined(jinja2.Undefined):
|
||||
"""Log on undefined variables."""
|
||||
|
||||
def _log_message(self):
|
||||
template = template_cv.get() or ""
|
||||
_LOGGER.warning(
|
||||
"Template variable warning: %s when rendering '%s'",
|
||||
self._undefined_message,
|
||||
template,
|
||||
)
|
||||
|
||||
def _fail_with_undefined_error(self, *args, **kwargs):
|
||||
try:
|
||||
return super()._fail_with_undefined_error(*args, **kwargs)
|
||||
except self._undefined_exception as ex:
|
||||
template = template_cv.get() or ""
|
||||
_LOGGER.error(
|
||||
"Template variable error: %s when rendering '%s'",
|
||||
self._undefined_message,
|
||||
template,
|
||||
)
|
||||
raise ex
|
||||
|
||||
def __str__(self):
|
||||
"""Log undefined __str___."""
|
||||
self._log_message()
|
||||
return super().__str__()
|
||||
|
||||
def __iter__(self):
|
||||
"""Log undefined __iter___."""
|
||||
self._log_message()
|
||||
return super().__iter__()
|
||||
|
||||
def __bool__(self):
|
||||
"""Log undefined __bool___."""
|
||||
self._log_message()
|
||||
return super().__bool__()
|
||||
|
||||
|
||||
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
"""The Home Assistant template environment."""
|
||||
|
||||
def __init__(self, hass, limited=False):
|
||||
"""Initialise template environment."""
|
||||
super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER))
|
||||
super().__init__(undefined=LoggingUndefined)
|
||||
self.hass = hass
|
||||
self.template_cache = weakref.WeakValueDictionary()
|
||||
self.filters["round"] = forgiving_round
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
PyJWT==1.7.1
|
||||
PyNaCl==1.3.0
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
aiohttp==3.7.4.post0
|
||||
aiohttp_cors==0.7.0
|
||||
astral==1.10.1
|
||||
@@ -16,7 +16,7 @@ defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.42.0
|
||||
home-assistant-frontend==20210402.0
|
||||
home-assistant-frontend==20210407.3
|
||||
httpx==0.17.1
|
||||
jinja2>=2.11.3
|
||||
netdisco==2.8.2
|
||||
|
||||
@@ -36,6 +36,7 @@ BASE_PLATFORMS = {
|
||||
"scene",
|
||||
"sensor",
|
||||
"switch",
|
||||
"tts",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
}
|
||||
|
||||
+14
-14
@@ -147,7 +147,7 @@ aioazuredevops==1.3.5
|
||||
aiobotocore==0.11.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -172,7 +172,7 @@ aioguardian==1.0.4
|
||||
aioharmony==0.2.7
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.2.60
|
||||
aiohomekit==0.2.61
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -224,7 +224,7 @@ aiopylgtv==0.4.0
|
||||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.6.1
|
||||
aioshelly==0.6.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
@@ -479,7 +479,7 @@ deluge-client==1.7.1
|
||||
denonavr==0.9.10
|
||||
|
||||
# homeassistant.components.devolo_home_control
|
||||
devolo-home-control-api==0.17.1
|
||||
devolo-home-control-api==0.17.3
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@@ -721,7 +721,7 @@ guppy3==3.1.0
|
||||
ha-ffmpeg==3.0.2
|
||||
|
||||
# homeassistant.components.philips_js
|
||||
ha-philipsjs==2.3.2
|
||||
ha-philipsjs==2.7.0
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.2.0
|
||||
@@ -763,7 +763,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210402.0
|
||||
home-assistant-frontend==20210407.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -916,7 +916,7 @@ magicseaweed==1.0.3
|
||||
matrix-client==0.3.2
|
||||
|
||||
# homeassistant.components.maxcube
|
||||
maxcube-api==0.4.1
|
||||
maxcube-api==0.4.2
|
||||
|
||||
# homeassistant.components.mythicbeastsdns
|
||||
mbddns==0.1.2
|
||||
@@ -986,7 +986,7 @@ netdisco==2.8.2
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.5
|
||||
nexia==0.9.6
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.1.0
|
||||
@@ -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
|
||||
@@ -1822,7 +1822,7 @@ python-qbittorrent==0.4.2
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.19
|
||||
python-smarttub==0.0.23
|
||||
|
||||
# homeassistant.components.sochain
|
||||
python-sochain-api==0.0.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
|
||||
|
||||
+17
-14
@@ -84,7 +84,7 @@ aioazuredevops==1.3.5
|
||||
aiobotocore==0.11.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.3.2
|
||||
aiodiscover==1.3.3
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -106,7 +106,7 @@ aioguardian==1.0.4
|
||||
aioharmony==0.2.7
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==0.2.60
|
||||
aiohomekit==0.2.61
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -143,7 +143,7 @@ aiopylgtv==0.4.0
|
||||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.6.1
|
||||
aioshelly==0.6.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
@@ -261,7 +261,7 @@ defusedxml==0.6.0
|
||||
denonavr==0.9.10
|
||||
|
||||
# homeassistant.components.devolo_home_control
|
||||
devolo-home-control-api==0.17.1
|
||||
devolo-home-control-api==0.17.3
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@@ -382,7 +382,7 @@ guppy3==3.1.0
|
||||
ha-ffmpeg==3.0.2
|
||||
|
||||
# homeassistant.components.philips_js
|
||||
ha-philipsjs==2.3.2
|
||||
ha-philipsjs==2.7.0
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.2.0
|
||||
@@ -412,7 +412,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210402.0
|
||||
home-assistant-frontend==20210407.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -476,7 +476,7 @@ logi_circle==0.2.2
|
||||
luftdaten==0.6.4
|
||||
|
||||
# homeassistant.components.maxcube
|
||||
maxcube-api==0.4.1
|
||||
maxcube-api==0.4.2
|
||||
|
||||
# homeassistant.components.mythicbeastsdns
|
||||
mbddns==0.1.2
|
||||
@@ -516,7 +516,10 @@ nessclient==0.9.15
|
||||
netdisco==2.8.2
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.5
|
||||
nexia==0.9.6
|
||||
|
||||
# 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
|
||||
@@ -956,7 +959,7 @@ python-nest==4.1.0
|
||||
python-openzwave-mqtt[mqtt-client]==1.4.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.19
|
||||
python-smarttub==0.0.23
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.12
|
||||
@@ -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
|
||||
|
||||
@@ -127,7 +127,7 @@ async def test_aemet_weather_create_sensors(hass):
|
||||
assert state.state == "Getafe"
|
||||
|
||||
state = hass.states.get("sensor.aemet_town_timestamp")
|
||||
assert state.state == "2021-01-09 11:47:45+00:00"
|
||||
assert state.state == "2021-01-09T11:47:45+00:00"
|
||||
|
||||
state = hass.states.get("sensor.aemet_wind_bearing")
|
||||
assert state.state == "90.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""The tests for the analytics ."""
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
@@ -13,10 +13,12 @@ from homeassistant.components.analytics.const import (
|
||||
ATTR_STATISTICS,
|
||||
ATTR_USAGE,
|
||||
)
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
from homeassistant.components.api import ATTR_UUID
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.loader import IntegrationNotFound
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
MOCK_HUUID = "abcdefg"
|
||||
MOCK_UUID = "abcdefg"
|
||||
|
||||
|
||||
async def test_no_send(hass, caplog, aioclient_mock):
|
||||
@@ -26,8 +28,7 @@ async def test_no_send(hass, caplog, aioclient_mock):
|
||||
with patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=False),
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.load()
|
||||
):
|
||||
assert not analytics.preferences[ATTR_BASE]
|
||||
|
||||
await analytics.send_analytics()
|
||||
@@ -76,9 +77,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock):
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert "Sending analytics failed with statuscode 400" in caplog.text
|
||||
|
||||
|
||||
@@ -88,9 +87,7 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock):
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert "Error sending analytics" in caplog.text
|
||||
|
||||
|
||||
@@ -98,12 +95,15 @@ async def test_send_base(hass, caplog, aioclient_mock):
|
||||
"""Test send base prefrences are defined."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.send_analytics()
|
||||
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||
|
||||
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
|
||||
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||
assert "'installation_type':" in caplog.text
|
||||
assert "'integration_count':" not in caplog.text
|
||||
@@ -131,10 +131,14 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock):
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
"uuid.UUID.hex", new_callable=PropertyMock
|
||||
) as hex:
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.load()
|
||||
|
||||
await analytics.send_analytics()
|
||||
assert f"'huuid': '{MOCK_HUUID}'" in caplog.text
|
||||
|
||||
assert f"'uuid': '{MOCK_UUID}'" in caplog.text
|
||||
assert f"'version': '{HA_VERSION}'" in caplog.text
|
||||
assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text
|
||||
assert "'installation_type':" in caplog.text
|
||||
@@ -147,12 +151,13 @@ async def test_send_usage(hass, caplog, aioclient_mock):
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||
|
||||
assert analytics.preferences[ATTR_BASE]
|
||||
assert analytics.preferences[ATTR_USAGE]
|
||||
hass.config.components = ["default_config"]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert "'integrations': ['default_config']" in caplog.text
|
||||
assert "'integration_count':" not in caplog.text
|
||||
|
||||
@@ -195,8 +200,6 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock):
|
||||
), patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
assert (
|
||||
@@ -215,8 +218,7 @@ async def test_send_statistics(hass, caplog, aioclient_mock):
|
||||
assert analytics.preferences[ATTR_STATISTICS]
|
||||
hass.config.components = ["default_config"]
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
assert (
|
||||
"'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0"
|
||||
in caplog.text
|
||||
@@ -236,11 +238,11 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.async_get_integration",
|
||||
side_effect=IntegrationNotFound("any"),
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
|
||||
post_call = aioclient_mock.mock_calls[0]
|
||||
assert "huuid" in post_call[2]
|
||||
assert "uuid" in post_call[2]
|
||||
assert post_call[2]["integration_count"] == 0
|
||||
|
||||
|
||||
@@ -258,7 +260,7 @@ async def test_send_statistics_async_get_integration_unknown_exception(
|
||||
with pytest.raises(ValueError), patch(
|
||||
"homeassistant.components.analytics.analytics.async_get_integration",
|
||||
side_effect=ValueError,
|
||||
), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID):
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
|
||||
|
||||
@@ -298,9 +300,36 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock):
|
||||
), patch(
|
||||
"homeassistant.components.hassio.is_hassio",
|
||||
side_effect=Mock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
assert "'addon_count': 1" in caplog.text
|
||||
assert "'integrations':" not in caplog.text
|
||||
|
||||
|
||||
async def test_reusing_uuid(hass, aioclient_mock):
|
||||
"""Test reusing the stored UUID."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
analytics._data[ATTR_UUID] = "NOT_MOCK_UUID"
|
||||
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
|
||||
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
|
||||
# This is not actually called but that in itself prove the test
|
||||
hex.return_value = MOCK_UUID
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert analytics.uuid == "NOT_MOCK_UUID"
|
||||
|
||||
|
||||
async def test_custom_integrations(hass, aioclient_mock):
|
||||
"""Test sending custom integrations."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
analytics = Analytics(hass)
|
||||
assert await async_setup_component(hass, "test_package", {"test_package": {}})
|
||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
|
||||
|
||||
await analytics.send_analytics()
|
||||
|
||||
payload = aioclient_mock.mock_calls[0][2]
|
||||
assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""The tests for the analytics ."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -22,11 +20,9 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json({"id": 1, "type": "analytics"})
|
||||
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||
response = await ws_client.receive_json()
|
||||
response = await ws_client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"]["huuid"] == "abcdef"
|
||||
|
||||
await ws_client.send_json(
|
||||
{"id": 2, "type": "analytics/preferences", "preferences": {"base": True}}
|
||||
@@ -36,7 +32,5 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock):
|
||||
assert response["result"]["preferences"]["base"]
|
||||
|
||||
await ws_client.send_json({"id": 3, "type": "analytics"})
|
||||
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||
response = await ws_client.receive_json()
|
||||
response = await ws_client.receive_json()
|
||||
assert response["result"]["preferences"]["base"]
|
||||
assert response["result"]["huuid"] == "abcdef"
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Tests for the Cast config flow."""
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import cast
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_creating_entry_sets_up_media_player(hass):
|
||||
"""Test setting up Cast loads the media player."""
|
||||
with patch(
|
||||
"homeassistant.components.cast.media_player.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
|
||||
), patch(
|
||||
"pychromecast.discovery.stop_discovery"
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
|
||||
async def test_single_instance(hass, source):
|
||||
"""Test we only allow a single config flow."""
|
||||
MockConfigEntry(domain="cast").add_to_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": source}
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_user_setup(hass):
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
async def test_user_setup_options(hass):
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
|
||||
)
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": ["192.168.0.1", "192.168.0.2"],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
async def test_zeroconf_setup(hass):
|
||||
"""Test we can finish a config flow through zeroconf."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "zeroconf"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
def get_suggested(schema, key):
|
||||
"""Get suggested value for key in voluptuous schema."""
|
||||
for k in schema.keys():
|
||||
if k == key:
|
||||
if k.description is None or "suggested_value" not in k.description:
|
||||
return None
|
||||
return k.description["suggested_value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"parameter_data",
|
||||
[
|
||||
(
|
||||
"known_hosts",
|
||||
["192.168.0.10", "192.168.0.11"],
|
||||
"192.168.0.10,192.168.0.11",
|
||||
"192.168.0.1, , 192.168.0.2 ",
|
||||
["192.168.0.1", "192.168.0.2"],
|
||||
),
|
||||
(
|
||||
"uuid",
|
||||
["bla", "blu"],
|
||||
"bla,blu",
|
||||
"foo, , bar ",
|
||||
["foo", "bar"],
|
||||
),
|
||||
(
|
||||
"ignore_cec",
|
||||
["cast1", "cast2"],
|
||||
"cast1,cast2",
|
||||
"other_cast, , some_cast ",
|
||||
["other_cast", "some_cast"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_option_flow(hass, parameter_data):
|
||||
"""Test config flow options."""
|
||||
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
|
||||
parameter, initial, suggested, user_input, updated = parameter_data
|
||||
|
||||
data = {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
}
|
||||
data[parameter] = initial
|
||||
config_entry = MockConfigEntry(domain="cast", data=data)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test ignore_cec and uuid options are hidden if advanced options are disabled
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "options"
|
||||
data_schema = result["data_schema"].schema
|
||||
assert set(data_schema) == {"known_hosts"}
|
||||
orig_data = dict(config_entry.data)
|
||||
|
||||
# Reconfigure ignore_cec, known_hosts, uuid
|
||||
context = {"source": "user", "show_advanced_options": True}
|
||||
result = await hass.config_entries.options.async_init(
|
||||
config_entry.entry_id, context=context
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "options"
|
||||
data_schema = result["data_schema"].schema
|
||||
for other_param in all_parameters:
|
||||
if other_param == parameter:
|
||||
continue
|
||||
assert get_suggested(data_schema, other_param) == ""
|
||||
assert get_suggested(data_schema, parameter) == suggested
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={parameter: user_input},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] is None
|
||||
for other_param in all_parameters:
|
||||
if other_param == parameter:
|
||||
continue
|
||||
assert config_entry.data[other_param] == []
|
||||
assert config_entry.data[parameter] == updated
|
||||
|
||||
# Clear known_hosts
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"known_hosts": ""},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] is None
|
||||
assert config_entry.data == {
|
||||
**orig_data,
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
|
||||
"""Test known hosts is passed to pychromecasts."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
await hass.async_block_till_done()
|
||||
config_entry = hass.config_entries.async_entries("cast")[0]
|
||||
|
||||
assert castbrowser_mock.start_discovery.call_count == 1
|
||||
castbrowser_constructor_mock.assert_called_once_with(
|
||||
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
|
||||
)
|
||||
castbrowser_mock.reset_mock()
|
||||
castbrowser_constructor_mock.reset_mock()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
castbrowser_mock.start_discovery.assert_not_called()
|
||||
castbrowser_constructor_mock.assert_not_called()
|
||||
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
|
||||
["192.168.0.11", "192.168.0.12"]
|
||||
)
|
||||
@@ -1,39 +1,9 @@
|
||||
"""Tests for the Cast config flow."""
|
||||
from unittest.mock import ANY, patch
|
||||
"""Tests for the Cast integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import cast
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_creating_entry_sets_up_media_player(hass):
|
||||
"""Test setting up Cast loads the media player."""
|
||||
with patch(
|
||||
"homeassistant.components.cast.media_player.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
|
||||
), patch(
|
||||
"pychromecast.discovery.stop_discovery"
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(hass, caplog):
|
||||
"""Test that specifying config will create an entry."""
|
||||
@@ -67,7 +37,7 @@ async def test_import(hass, caplog):
|
||||
|
||||
|
||||
async def test_not_configuring_cast_not_creates_entry(hass):
|
||||
"""Test that no config will not create an entry."""
|
||||
"""Test that an empty config does not create an entry."""
|
||||
with patch(
|
||||
"homeassistant.components.cast.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
@@ -75,207 +45,3 @@ async def test_not_configuring_cast_not_creates_entry(hass):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_setup.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
|
||||
async def test_single_instance(hass, source):
|
||||
"""Test we only allow a single config flow."""
|
||||
MockConfigEntry(domain="cast").add_to_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": source}
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_user_setup(hass):
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
async def test_user_setup_options(hass):
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
|
||||
)
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": ["192.168.0.1", "192.168.0.2"],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
async def test_zeroconf_setup(hass):
|
||||
"""Test we can finish a config flow through zeroconf."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "zeroconf"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == 1
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].data == {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
"user_id": users[0].id, # Home Assistant cast user
|
||||
}
|
||||
|
||||
|
||||
def get_suggested(schema, key):
|
||||
"""Get suggested value for key in voluptuous schema."""
|
||||
for k in schema.keys():
|
||||
if k == key:
|
||||
if k.description is None or "suggested_value" not in k.description:
|
||||
return None
|
||||
return k.description["suggested_value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"parameter_data",
|
||||
[
|
||||
(
|
||||
"known_hosts",
|
||||
["192.168.0.10", "192.168.0.11"],
|
||||
"192.168.0.10,192.168.0.11",
|
||||
"192.168.0.1, , 192.168.0.2 ",
|
||||
["192.168.0.1", "192.168.0.2"],
|
||||
),
|
||||
(
|
||||
"uuid",
|
||||
["bla", "blu"],
|
||||
"bla,blu",
|
||||
"foo, , bar ",
|
||||
["foo", "bar"],
|
||||
),
|
||||
(
|
||||
"ignore_cec",
|
||||
["cast1", "cast2"],
|
||||
"cast1,cast2",
|
||||
"other_cast, , some_cast ",
|
||||
["other_cast", "some_cast"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_option_flow(hass, parameter_data):
|
||||
"""Test config flow options."""
|
||||
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
|
||||
parameter, initial, suggested, user_input, updated = parameter_data
|
||||
|
||||
data = {
|
||||
"ignore_cec": [],
|
||||
"known_hosts": [],
|
||||
"uuid": [],
|
||||
}
|
||||
data[parameter] = initial
|
||||
config_entry = MockConfigEntry(domain="cast", data=data)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test ignore_cec and uuid options are hidden if advanced options are disabled
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "options"
|
||||
data_schema = result["data_schema"].schema
|
||||
assert set(data_schema) == {"known_hosts"}
|
||||
|
||||
# Reconfigure ignore_cec, known_hosts, uuid
|
||||
context = {"source": "user", "show_advanced_options": True}
|
||||
result = await hass.config_entries.options.async_init(
|
||||
config_entry.entry_id, context=context
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "options"
|
||||
data_schema = result["data_schema"].schema
|
||||
for other_param in all_parameters:
|
||||
if other_param == parameter:
|
||||
continue
|
||||
assert get_suggested(data_schema, other_param) == ""
|
||||
assert get_suggested(data_schema, parameter) == suggested
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={parameter: user_input},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] is None
|
||||
for other_param in all_parameters:
|
||||
if other_param == parameter:
|
||||
continue
|
||||
assert config_entry.data[other_param] == []
|
||||
assert config_entry.data[parameter] == updated
|
||||
|
||||
# Clear known_hosts
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"known_hosts": ""},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] is None
|
||||
assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []}
|
||||
|
||||
|
||||
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
|
||||
"""Test known hosts is passed to pychromecasts."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"cast", context={"source": "user"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
await hass.async_block_till_done()
|
||||
config_entry = hass.config_entries.async_entries("cast")[0]
|
||||
|
||||
assert castbrowser_mock.start_discovery.call_count == 1
|
||||
castbrowser_constructor_mock.assert_called_once_with(
|
||||
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
|
||||
)
|
||||
castbrowser_mock.reset_mock()
|
||||
castbrowser_constructor_mock.reset_mock()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
castbrowser_mock.start_discovery.assert_not_called()
|
||||
castbrowser_constructor_mock.assert_not_called()
|
||||
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
|
||||
["192.168.0.11", "192.168.0.12"]
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ async def test_log_filtering(hass, caplog):
|
||||
"doesntmatchanything",
|
||||
".*shouldfilterall.*",
|
||||
"^filterthis:.*",
|
||||
"in the middle",
|
||||
],
|
||||
"test.other_filter": [".*otherfilterer"],
|
||||
},
|
||||
@@ -62,6 +63,7 @@ async def test_log_filtering(hass, caplog):
|
||||
filter_logger, False, "this line containing shouldfilterall should be filtered"
|
||||
)
|
||||
msg_test(filter_logger, True, "this line should not be filtered filterthis:")
|
||||
msg_test(filter_logger, False, "this in the middle should be filtered")
|
||||
msg_test(filter_logger, False, "filterthis: should be filtered")
|
||||
msg_test(filter_logger, False, "format string shouldfilter%s", "all")
|
||||
msg_test(filter_logger, True, "format string shouldfilter%s", "not")
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.maxcube import DOMAIN
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -105,5 +106,5 @@ async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutte
|
||||
assert await async_setup_component(hass, DOMAIN, hass_config)
|
||||
await hass.async_block_till_done()
|
||||
gateway = hass_config[DOMAIN]["gateways"][0]
|
||||
mock.assert_called_with(gateway["host"], gateway.get("port", 62910))
|
||||
mock.assert_called_with(gateway["host"], gateway.get("port", 62910), now=now)
|
||||
return cube
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test Media Source initialization."""
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import quote
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -45,7 +46,7 @@ async def test_async_browse_media(hass):
|
||||
media = await media_source.async_browse_media(hass, "")
|
||||
assert isinstance(media, media_source.models.BrowseMediaSource)
|
||||
assert media.title == "media/"
|
||||
assert len(media.children) == 1
|
||||
assert len(media.children) == 2
|
||||
|
||||
# Test invalid media content
|
||||
with pytest.raises(ValueError):
|
||||
@@ -133,14 +134,15 @@ async def test_websocket_browse_media(hass, hass_ws_client):
|
||||
assert msg["error"]["message"] == "test"
|
||||
|
||||
|
||||
async def test_websocket_resolve_media(hass, hass_ws_client):
|
||||
@pytest.mark.parametrize("filename", ["test.mp3", "Epic Sax Guy 10 Hours.mp4"])
|
||||
async def test_websocket_resolve_media(hass, hass_ws_client, filename):
|
||||
"""Test browse media websocket."""
|
||||
assert await async_setup_component(hass, const.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg")
|
||||
media = media_source.models.PlayMedia(f"/media/local/{filename}", "audio/mpeg")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
@@ -150,7 +152,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_source/resolve_media",
|
||||
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3",
|
||||
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/{filename}",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,7 +160,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["id"] == 1
|
||||
assert msg["result"]["url"].startswith(media.url)
|
||||
assert msg["result"]["url"].startswith(quote(media.url))
|
||||
assert msg["result"]["mime_type"] == media.mime_type
|
||||
|
||||
with patch(
|
||||
|
||||
@@ -95,5 +95,8 @@ async def test_media_view(hass, hass_client):
|
||||
resp = await client.get("/media/local/test.mp3")
|
||||
assert resp.status == 200
|
||||
|
||||
resp = await client.get("/media/local/Epic Sax Guy 10 Hours.mp4")
|
||||
assert resp.status == 200
|
||||
|
||||
resp = await client.get("/media/recordings/test.mp3")
|
||||
assert resp.status == 200
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user