Compare commits

..

36 Commits

Author SHA1 Message Date
Paulus Schoutsen 172a02a605 Bumped version to 0.115.0b7 2020-09-12 12:28:57 +00:00
Bram Kragten b6f868f629 Add children media class to children spotify media browser (#39953) 2020-09-12 12:28:47 +00:00
uvjustin 5697f4b4e7 Set output timescale to input timescale (#39946) 2020-09-12 12:28:47 +00:00
J. Nick Koston 30f9e1b479 Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943) 2020-09-12 12:28:46 +00:00
Quentame fcbcebea9b Fix missing position attribute for MeteoFranceAlertSensor (#39938) 2020-09-12 12:28:45 +00:00
J. Nick Koston f81606cbf5 Return the listeners with the template result for the websocket api (#39925) 2020-09-12 12:28:44 +00:00
Greg Dowling 3240be0bb6 Bump pyloopenergy library to 0.2.1 (#39919) 2020-09-12 12:28:44 +00:00
On Freund 18be6cbadc Handle Kodi shutdown (#39856)
* Handle Kodi shutdown

* Core review comments

* Make async_on_quit a coroutine
2020-09-12 12:28:43 +00:00
Paulus Schoutsen a002e9b12f Bumped version to 0.115.0b6 2020-09-11 12:18:53 +00:00
Franck Nijhof db64a9ebfa Accept known hosts for get_url for OAuth (#39936) 2020-09-11 12:17:24 +00:00
J. Nick Koston 3fbde22cc4 Update zeroconf to 0.28.5 (#39923) 2020-09-11 12:17:24 +00:00
J. Nick Koston 758e60a58d Prevent missing integration from failing HomeKit startup (#39918) 2020-09-11 12:17:23 +00:00
Paulus Schoutsen 5201410e39 Bump aioshelly to 0.3.1 (#39917) 2020-09-11 12:16:43 +00:00
Thomas Lovén b1b7944012 Set variable values in scripts (#39915)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:16:01 +00:00
Paulus Schoutsen 8ef04268be Extract variable rendering (#39934) 2020-09-11 12:15:28 +00:00
Bas Nijholt b107e87d38 Don't trigger on attribute when the attribute doesn't change (#39910)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:12:58 +00:00
uvjustin b0b9579778 Disable audio for HLS or mpegts input (#39906) 2020-09-11 12:12:57 +00:00
Martin Hjelmare 7eade4029a Add children media class (#39902)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:12:57 +00:00
Marvin Wichmann 3d4913348a Warn users if KNX has no devices configured (#39899)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-09-11 12:12:56 +00:00
J. Nick Koston 1720b71d62 Limit zeroconf discovery to name/macaddress when provided (#39877)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-09-11 12:12:27 +00:00
Paulus Schoutsen 589086f0d0 Bumped version to 0.115.0b5 2020-09-10 18:53:37 +00:00
Erik Montnemery 6f8060dea7 Fix discovery update of MQTT state templates (#39901) 2020-09-10 18:53:25 +00:00
J. Nick Koston b8ef87d84c Fix ping log level to be debug instead of warning (#39900) 2020-09-10 18:53:25 +00:00
J. Nick Koston 7370b0ffc6 Detect self-referencing loops in template entities and log a warning (#39897) 2020-09-10 18:53:24 +00:00
Paulus Schoutsen 209cf44e8e Add default variables to script helper (#39895) 2020-09-10 18:53:23 +00:00
Pascal Vizeli b7dacabbe4 Fix issue with grpcio build on 32bit arch (#39893) 2020-09-10 18:53:22 +00:00
Bram Kragten 5098c35814 Fix spotify media browser category (#39888) 2020-09-10 18:53:21 +00:00
Paulus Schoutsen 896df60f32 Shelly switch to guard for shelly 2 in roller mode (#39886) 2020-09-10 18:53:21 +00:00
Pascal Vizeli b26ab2849b Bump hass-nabucasa 0.37.0 (#39885) 2020-09-10 18:53:20 +00:00
Paulus Schoutsen 36f52a26f6 Fix event trigger (#39884)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2020-09-10 18:53:19 +00:00
bsmappee f0295d562d Bump pysmappee to 0.2.13 (#39883) 2020-09-10 18:53:18 +00:00
Joakim Sørensen 081bd22e59 Updated frontend to 20200909.0 (#39869)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-10 18:53:18 +00:00
Franck Nijhof 668c73010a Disable Met.no hourly weather by default (#39867)
Co-authored-by: Daniel Hjelseth Høyer <mail@dahoiv.net>
2020-09-10 18:53:17 +00:00
Paulus Schoutsen fe371f0438 Install stdlib-list in script/bootstrap (#39866) 2020-09-10 18:53:16 +00:00
Joakim Sørensen be28dc0bca Add exception for NoURLAvailableError in OAuth2FlowHandler (#39845)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-10 18:53:15 +00:00
Chris Talkington 4578baca3e Improve Roku media browser structure (#39754)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-09-10 18:53:15 +00:00
98 changed files with 2822 additions and 470 deletions
+1
View File
@@ -236,6 +236,7 @@ homeassistant/components/linux_battery/* @fabaff
homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/loopenergy/* @pavoni
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl @mzdrale
homeassistant/components/luftdaten/* @fabaff
+7
View File
@@ -49,6 +49,7 @@ jobs:
builderVersion: '$(versionWheels)'
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
builderPip: 'Cython;numpy;scikit-build'
builderEnvFile: true
skipBinary: 'aiohttp'
wheelsRequirement: 'requirements_wheels.txt'
wheelsRequirementDiff: 'requirements_diff.txt'
@@ -90,4 +91,10 @@ jobs:
sed -i "s|# bme680|bme680|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
done
# Write env for build settings
(
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=0"
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1"
) > .env_file
displayName: 'Prepare requirements files for Home Assistant wheels'
+2 -1
View File
@@ -10,7 +10,8 @@
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
"missing_configuration": "Please check the documentation on how to set up Almond."
"missing_configuration": "Please check the documentation on how to set up Almond.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
}
}
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_ID,
CONF_MODE,
CONF_PLATFORM,
CONF_VARIABLES,
CONF_ZONE,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
@@ -29,7 +30,7 @@ from homeassistant.core import (
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition, extract_domain_configs
from homeassistant.helpers import condition, extract_domain_configs, template
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
@@ -44,6 +45,7 @@ from homeassistant.helpers.script import (
Script,
make_script_schema,
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
@@ -104,6 +106,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
},
SCRIPT_MODE_SINGLE,
@@ -239,6 +242,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
cond_func,
action_script,
initial_state,
variables,
):
"""Initialize an automation entity."""
self._id = automation_id
@@ -253,6 +257,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None
self._logger = _LOGGER
self._variables: ScriptVariables = variables
@property
def name(self):
@@ -378,11 +383,20 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
else:
await self.async_disable()
async def async_trigger(self, variables, context=None, skip_condition=False):
async def async_trigger(self, run_variables, context=None, skip_condition=False):
"""Trigger automation.
This method is a coroutine.
"""
if self._variables:
try:
variables = self._variables.async_render(self.hass, run_variables)
except template.TemplateError as err:
self._logger.error("Error rendering variables: %s", err)
return
else:
variables = run_variables
if (
not skip_condition
and self._cond_func is not None
@@ -518,6 +532,9 @@ async def _async_process_config(hass, config, component):
max_runs=config_block[CONF_MAX],
max_exceeded=config_block[CONF_MAX_EXCEEDED],
logger=_LOGGER,
# We don't pass variables here
# Automation will already render them to use them in the condition
# and so will pass them on to the script.
)
if CONF_CONDITION in config_block:
@@ -535,6 +552,7 @@ async def _async_process_config(hass, config, component):
cond_func,
action_script,
initial_state,
config_block.get(CONF_VARIABLES),
)
entities.append(entity)
+5 -1
View File
@@ -4,7 +4,11 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==35"],
"zeroconf": ["_axis-video._tcp.local."],
"zeroconf": [
{"type":"_axis-video._tcp.local.","macaddress":"00408C*"},
{"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"},
{"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"}
],
"after_dependencies": ["mqtt"],
"codeowners": ["@Kane610"]
}
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/brother",
"codeowners": ["@bieniu"],
"requirements": ["brother==0.1.17"],
"zeroconf": ["_printer._tcp.local."],
"zeroconf": [{"type": "_printer._tcp.local.", "name":"brother*"}],
"config_flow": true,
"quality_scale": "platinum"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "cloud",
"name": "Home Assistant Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud",
"requirements": ["hass-nabucasa==0.36.1"],
"requirements": ["hass-nabucasa==0.37.0"],
"dependencies": ["http", "webhook", "alexa"],
"after_dependencies": ["google_assistant"],
"codeowners": ["@home-assistant/cloud"]
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/doorbird",
"requirements": ["doorbirdpy==2.1.0"],
"dependencies": ["http"],
"zeroconf": ["_axis-video._tcp.local."],
"zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}],
"codeowners": ["@oblogic7", "@bdraco"],
"config_flow": true
}
@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20200908.0"],
"requirements": ["home-assistant-frontend==20200909.0"],
"dependencies": [
"api",
"auth",
@@ -6,7 +6,8 @@
}
},
"abort": {
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation."
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
"default": "Successfully authenticated with Home Connect."
@@ -28,11 +28,15 @@ async def async_attach_trigger(
):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
event_data_schema = (
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
if config.get(CONF_EVENT_DATA)
else None
)
event_data_schema = None
if config.get(CONF_EVENT_DATA):
event_data_schema = vol.Schema(
{
vol.Required(key): value
for key, value in config.get(CONF_EVENT_DATA).items()
},
extra=vol.ALLOW_EXTRA,
)
@callback
def handle_event(event):
@@ -80,6 +80,13 @@ async def async_attach_trigger(
else:
new_value = to_s.attributes.get(attribute)
# When we listen for state changes with `match_all`, we
# will trigger even if just an attribute changes. When
# we listen to just an attribute, we should ignore all
# other attribute changes.
if attribute is not None and old_value == new_value:
return
if (
not match_from_state(old_value)
or not match_to_state(new_value)
+8 -3
View File
@@ -38,7 +38,7 @@ from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.loader import async_get_integration
from homeassistant.loader import IntegrationNotFound, async_get_integration
from homeassistant.util import get_local_ip
from .accessories import get_accessory
@@ -712,8 +712,13 @@ class HomeKit:
if dev_reg_ent.sw_version:
ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version
if ATTR_MANUFACTURER not in ent_cfg:
integration = await async_get_integration(self.hass, ent_reg_ent.platform)
ent_cfg[ATTR_INTERGRATION] = integration.name
try:
integration = await async_get_integration(
self.hass, ent_reg_ent.platform
)
ent_cfg[ATTR_INTERGRATION] = integration.name
except IntegrationNotFound:
ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform
class HomeKitPairingQRView(HomeAssistantView):
+6
View File
@@ -148,6 +148,12 @@ async def async_setup(hass, config):
discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config)
)
if not hass.data[DATA_KNX].xknx.devices:
_LOGGER.warning(
"No KNX devices are configured. Please read "
"https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes"
)
hass.services.async_register(
DOMAIN,
SERVICE_KNX_SEND,
+29 -10
View File
@@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_TRACK,
]
CONTENT_TYPE_MEDIA_CLASS = {
"library_music": MEDIA_CLASS_MUSIC,
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
}
CHILD_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
@@ -151,8 +158,10 @@ async def build_item_response(media_library, payload):
except UnknownMediaType:
pass
return BrowseMedia(
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
response = BrowseMedia(
media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
search_type, MEDIA_CLASS_DIRECTORY
),
media_content_id=search_id,
media_content_type=search_type,
title=title,
@@ -162,6 +171,13 @@ async def build_item_response(media_library, payload):
thumbnail=thumbnail,
)
if search_type == "library_music":
response.children_media_class = MEDIA_CLASS_MUSIC
else:
response.calculate_children_class()
return response
def item_payload(item, media_library):
"""
@@ -170,11 +186,12 @@ def item_payload(item, media_library):
Used by async_browse_media.
"""
title = item["label"]
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = media_library.thumbnail_url(thumbnail)
media_class = None
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
@@ -213,16 +230,18 @@ def item_payload(item, media_library):
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_class = MEDIA_CLASS_DIRECTORY
media_content_type = item["type"]
media_content_id = ""
can_play = False
can_expand = True
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
if media_class is None:
try:
media_class = CHILD_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
return BrowseMedia(
title=title,
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "kodi",
"name": "Kodi",
"documentation": "https://www.home-assistant.io/integrations/kodi",
"requirements": ["pykodi==0.1.2"],
"requirements": ["pykodi==0.2.0"],
"codeowners": [
"@OnFreund"
],
+20 -6
View File
@@ -5,6 +5,7 @@ import logging
import re
import jsonrpc_base
from pykodi import CannotConnectError
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@@ -324,11 +325,15 @@ class KodiEntity(MediaPlayerEntity):
self._app_properties["muted"] = data["muted"]
self.async_write_ha_state()
@callback
def async_on_quit(self, sender, data):
async def async_on_quit(self, sender, data):
"""Reset the player state on quit action."""
await self._clear_connection()
async def _clear_connection(self, close=True):
self._reset_state()
self.hass.async_create_task(self._connection.close())
self.async_write_ha_state()
if close:
await self._connection.close()
@property
def unique_id(self):
@@ -386,14 +391,23 @@ class KodiEntity(MediaPlayerEntity):
try:
await self._connection.connect()
self._on_ws_connected()
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.info("Unable to connect to Kodi via websocket")
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True)
await self._clear_connection(False)
async def _ping(self):
try:
await self._kodi.ping()
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True)
await self._clear_connection()
async def _async_connect_websocket_if_disconnected(self, *_):
"""Reconnect the websocket if it fails."""
if not self._connection.connected:
await self._async_ws_connect()
else:
await self._ping()
@callback
def _register_ws_callbacks(self):
@@ -464,7 +478,7 @@ class KodiEntity(MediaPlayerEntity):
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return (not self._connection.can_subscribe) or (not self._connection.connected)
return not self._connection.can_subscribe
@property
def volume_level(self):
@@ -2,6 +2,8 @@
"domain": "loopenergy",
"name": "Loop Energy",
"documentation": "https://www.home-assistant.io/integrations/loopenergy",
"requirements": ["pyloopenergy==0.1.3"],
"codeowners": []
"requirements": ["pyloopenergy==0.2.1"],
"codeowners": [
"@pavoni"
]
}
@@ -85,6 +85,7 @@ from .const import (
ATTR_SOUND_MODE,
ATTR_SOUND_MODE_LIST,
DOMAIN,
MEDIA_CLASS_DIRECTORY,
SERVICE_CLEAR_PLAYLIST,
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE,
@@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity):
media_content_type: Optional[str] = None,
media_content_id: Optional[str] = None,
) -> "BrowseMedia":
"""
Return a payload for the "media_player/browse_media" websocket command.
"""Return a BrowseMedia instance.
Payload should follow this format:
{
"title": str - Title of the item
"media_class": str - Media class
"media_content_type": str - see below
"media_content_id": str - see below
- Can be passed back in to browse further
- Can be used as-is with media_player.play_media service
"can_play": bool - If item is playable
"can_expand": bool - If item contains other media
"thumbnail": str (Optional) - URL to image thumbnail for item
"children": list (Optional) - [{<item_with_keys_above>}, ...]
}
Note: Children should omit the children key.
The BrowseMedia instance will be used by the
"media_player/browse_media" websocket command.
"""
raise NotImplementedError()
@@ -1054,6 +1041,7 @@ class BrowseMedia:
can_play: bool,
can_expand: bool,
children: Optional[List["BrowseMedia"]] = None,
children_media_class: Optional[str] = None,
thumbnail: Optional[str] = None,
):
"""Initialize browse media item."""
@@ -1064,10 +1052,14 @@ class BrowseMedia:
self.can_play = can_play
self.can_expand = can_expand
self.children = children
self.children_media_class = children_media_class
self.thumbnail = thumbnail
def as_dict(self, *, parent: bool = True) -> dict:
"""Convert Media class to browse media dictionary."""
if self.children_media_class is None:
self.calculate_children_class()
response = {
"title": self.title,
"media_class": self.media_class,
@@ -1075,6 +1067,7 @@ class BrowseMedia:
"media_content_id": self.media_content_id,
"can_play": self.can_play,
"can_expand": self.can_expand,
"children_media_class": self.children_media_class,
"thumbnail": self.thumbnail,
}
@@ -1089,3 +1082,14 @@ class BrowseMedia:
response["children"] = []
return response
def calculate_children_class(self) -> None:
"""Count the children media classes and calculate the correct class."""
if self.children is None or len(self.children) == 0:
return
self.children_media_class = MEDIA_CLASS_DIRECTORY
proposed_class = self.children[0].media_class
if all(child.media_class == proposed_class for child in self.children):
self.children_media_class = proposed_class
@@ -6,6 +6,7 @@ from typing import List, Optional, Tuple
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
@@ -53,11 +54,12 @@ class MediaSourceItem:
base = BrowseMediaSource(
domain=None,
identifier=None,
media_class=MEDIA_CLASS_CHANNEL,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type=MEDIA_TYPE_CHANNELS,
title="Media Sources",
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_CHANNEL,
)
base.children = [
BrowseMediaSource(
+16 -6
View File
@@ -104,7 +104,6 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
self._config = config
self._is_metric = is_metric
self._hourly = hourly
self._name_appendix = "-hourly" if hourly else ""
@property
def track_home(self):
@@ -114,23 +113,34 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
@property
def unique_id(self):
"""Return unique ID."""
name_appendix = ""
if self._hourly:
name_appendix = "-hourly"
if self.track_home:
return f"home{self._name_appendix}"
return f"home{name_appendix}"
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}"
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}"
@property
def name(self):
"""Return the name of the sensor."""
name = self._config.get(CONF_NAME)
name_appendix = ""
if self._hourly:
name_appendix = " Hourly"
if name is not None:
return f"{name}{self._name_appendix}"
return f"{name}{name_appendix}"
if self.track_home:
return f"{self.hass.config.location_name}{self._name_appendix}"
return f"{self.hass.config.location_name}{name_appendix}"
return f"{DEFAULT_NAME}{self._name_appendix}"
return f"{DEFAULT_NAME}{name_appendix}"
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return not self._hourly
@property
def condition(self):
@@ -79,9 +79,10 @@ class MeteoFranceSensor(CoordinatorEntity):
"""Initialize the Meteo-France sensor."""
super().__init__(coordinator)
self._type = sensor_type
city_name = self.coordinator.data.position["name"]
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
if hasattr(self.coordinator.data, "position"):
city_name = self.coordinator.data.position["name"]
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
@property
def unique_id(self):
+1 -1
View File
@@ -1305,7 +1305,7 @@ class MqttDiscoveryUpdate(Entity):
debug_info.add_entity_discovery_data(
self.hass, self._discovery_data, self.entity_id
)
# Set in case the entity has been removed and is re-added
# Set in case the entity has been removed and is re-added, for example when changing entity_id
set_discovery_hash(self.hass, discovery_hash)
self._remove_signal = async_dispatcher_connect(
self.hass,
@@ -104,7 +104,7 @@ async def async_setup_platform(
):
"""Set up MQTT alarm control panel through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -128,10 +128,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(config, config_entry, discovery_data)])
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
class MqttAlarm(
@@ -143,13 +143,16 @@ class MqttAlarm(
):
"""Representation of a MQTT alarm status."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Init the MQTT Alarm Control Panel."""
self.hass = hass
self._state = None
self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -165,26 +168,30 @@ class MqttAlarm(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
command_template = self._config[CONF_COMMAND_TEMPLATE]
command_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Run when new MQTT message has been received."""
payload = msg.payload
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
msg.payload, self._state
+16 -9
View File
@@ -76,7 +76,7 @@ async def async_setup_platform(
):
"""Set up MQTT binary sensor through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -100,10 +100,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT binary sensor."""
async_add_entities([MqttBinarySensor(config, config_entry, discovery_data)])
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
class MqttBinarySensor(
@@ -115,9 +115,9 @@ class MqttBinarySensor(
):
"""Representation a binary sensor that is updated by MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT binary sensor."""
self._config = config
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
@@ -128,6 +128,10 @@ class MqttBinarySensor(
self._expired = True
else:
self._expired = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -143,19 +147,22 @@ class MqttBinarySensor(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
+11 -7
View File
@@ -174,7 +174,7 @@ async def async_setup_platform(
):
"""Set up MQTT cover through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -186,7 +186,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -198,10 +198,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Cover."""
async_add_entities([MqttCover(config, config_entry, discovery_data)])
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])
class MqttCover(
@@ -213,8 +213,9 @@ class MqttCover(
):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the cover."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._position = None
self._state = None
@@ -257,8 +258,6 @@ class MqttCover(
)
self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
@@ -269,6 +268,8 @@ class MqttCover(
if tilt_status_template is not None:
tilt_status_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
@callback
@@ -276,6 +277,7 @@ class MqttCover(
def tilt_message_received(msg):
"""Handle tilt updates."""
payload = msg.payload
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
if tilt_status_template is not None:
payload = tilt_status_template.async_render_with_possible_json_value(
payload
@@ -296,6 +298,7 @@ class MqttCover(
def state_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
@@ -321,6 +324,7 @@ class MqttCover(
def position_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
+16 -15
View File
@@ -115,7 +115,7 @@ async def async_setup_platform(
):
"""Set up MQTT fan through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -127,7 +127,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -139,10 +139,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT fan."""
async_add_entities([MqttFan(config, config_entry, discovery_data)])
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])
class MqttFan(
@@ -154,8 +154,9 @@ class MqttFan(
):
"""A MQTT fan component."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT fan."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = False
self._speed = None
@@ -242,22 +243,22 @@ class MqttFan(
self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED
)
for key, tpl in list(self._templates.items()):
if tpl is None:
self._templates[key] = lambda value: value
else:
tpl.hass = self.hass
self._templates[key] = tpl.async_render_with_possible_json_value
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
templates = {}
for key, tpl in list(self._templates.items()):
if tpl is None:
templates[key] = lambda value: value
else:
tpl.hass = self.hass
templates[key] = tpl.async_render_with_possible_json_value
@callback
@log_messages(self.hass, self.entity_id)
def state_received(msg):
"""Handle new received MQTT message."""
payload = templates[CONF_STATE](msg.payload)
payload = self._templates[CONF_STATE](msg.payload)
if payload == self._payload["STATE_ON"]:
self._state = True
elif payload == self._payload["STATE_OFF"]:
@@ -275,7 +276,7 @@ class MqttFan(
@log_messages(self.hass, self.entity_id)
def speed_received(msg):
"""Handle new received MQTT message for the speed."""
payload = templates[ATTR_SPEED](msg.payload)
payload = self._templates[ATTR_SPEED](msg.payload)
if payload == self._payload["SPEED_LOW"]:
self._speed = SPEED_LOW
elif payload == self._payload["SPEED_MEDIUM"]:
@@ -298,7 +299,7 @@ class MqttFan(
@log_messages(self.hass, self.entity_id)
def oscillation_received(msg):
"""Handle new received MQTT message for the oscillation."""
payload = templates[OSCILLATION](msg.payload)
payload = self._templates[OSCILLATION](msg.payload)
if payload == self._payload["OSCILLATE_ON_PAYLOAD"]:
self._oscillation = True
elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]:
@@ -254,7 +254,7 @@ class MqttLight(
value_templates = {}
for key in VALUE_TEMPLATE_KEYS:
value_templates[key] = lambda value: value
value_templates[key] = lambda value, _: value
for key in VALUE_TEMPLATE_KEYS & config.keys():
tpl = config[key]
value_templates[key] = tpl.async_render_with_possible_json_value
@@ -304,7 +304,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def state_received(msg):
"""Handle new MQTT messages."""
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
return
@@ -328,7 +330,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def brightness_received(msg):
"""Handle new MQTT messages for the brightness."""
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic)
return
@@ -360,7 +364,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def rgb_received(msg):
"""Handle new MQTT messages for RGB."""
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic)
return
@@ -392,7 +396,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def color_temp_received(msg):
"""Handle new MQTT messages for color temperature."""
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic)
return
@@ -422,7 +428,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def effect_received(msg):
"""Handle new MQTT messages for effect."""
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic)
return
@@ -452,7 +460,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def hs_received(msg):
"""Handle new MQTT messages for hs color."""
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic)
return
@@ -484,7 +492,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def white_value_received(msg):
"""Handle new MQTT messages for white value."""
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic)
return
@@ -516,7 +526,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def xy_received(msg):
"""Handle new MQTT messages for xy color."""
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic)
return
+10 -7
View File
@@ -77,7 +77,7 @@ async def async_setup_platform(
):
"""Set up MQTT lock panel through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -89,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -101,10 +101,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Lock platform."""
async_add_entities([MqttLock(config, config_entry, discovery_data)])
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])
class MqttLock(
@@ -116,8 +116,9 @@ class MqttLock(
):
"""Representation of a lock that can be toggled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the lock."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = False
self._sub_state = None
@@ -154,17 +155,19 @@ class MqttLock(
self._optimistic = config[CONF_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Handle new MQTT messages."""
payload = msg.payload
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(payload)
if payload == self._config[CONF_STATE_LOCKED]:
+18 -9
View File
@@ -70,7 +70,7 @@ async def async_setup_platform(
):
"""Set up MQTT sensors through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -94,10 +94,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up MQTT sensor."""
async_add_entities([MqttSensor(config, config_entry, discovery_data)])
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
class MqttSensor(
@@ -105,9 +105,9 @@ class MqttSensor(
):
"""Representation of a sensor that can be updated using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the sensor."""
self._config = config
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
@@ -118,6 +118,10 @@ class MqttSensor(
self._expired = True
else:
self._expired = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -133,19 +137,23 @@ class MqttSensor(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._config = config
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
@@ -169,6 +177,7 @@ class MqttSensor(
self.hass, self._value_is_expired, expiration_at
)
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(
payload, self._state
+10 -7
View File
@@ -73,7 +73,7 @@ async def async_setup_platform(
):
"""Set up MQTT switch through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities, discovery_info)
await _async_setup_entity(hass, config, async_add_entities, discovery_info)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -85,7 +85,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -97,10 +97,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT switch."""
async_add_entities([MqttSwitch(config, config_entry, discovery_data)])
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])
class MqttSwitch(
@@ -113,8 +113,9 @@ class MqttSwitch(
):
"""Representation of a switch that can be toggled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT switch."""
self.hass = hass
self._state = False
self._sub_state = None
@@ -160,17 +161,19 @@ class MqttSwitch(
self._optimistic = config[CONF_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def state_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
if payload == self._state_on:
@@ -5,6 +5,7 @@ import re
from typing import Optional, Tuple
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_VIDEO,
)
@@ -91,10 +92,12 @@ class NetatmoSource(MediaSource):
else:
path = f"{source}/{camera_id}"
media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO
media = BrowseMediaSource(
domain=DOMAIN,
identifier=path,
media_class=MEDIA_CLASS_VIDEO,
media_class=media_class,
media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(
@@ -8,7 +8,8 @@
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]"
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
@@ -39,4 +40,4 @@
}
}
}
}
}
@@ -12,6 +12,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
@@ -289,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNEL,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type=MEDIA_TYPE_CHANNELS,
can_play=False,
@@ -134,7 +134,7 @@ class PingDataICMPLib(PingData):
async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
_LOGGER.warning("ping address: %s", self._ip_address)
_LOGGER.debug("ping address: %s", self._ip_address)
data = await self.hass.async_add_executor_job(
partial(
icmp_ping,
+34 -6
View File
@@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError):
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
PLAYLISTS_BROWSE_PAYLOAD = {
"title": "Playlists",
"media_class": MEDIA_CLASS_PLAYLIST,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": "all",
"media_content_type": "playlists",
"can_play": False,
@@ -94,10 +94,21 @@ def browse_media(
if special_folder:
if media_content_type == "server":
library_or_section = plex_server.library
children_media_class = MEDIA_CLASS_DIRECTORY
title = plex_server.friendly_name
elif media_content_type == "library":
library_or_section = plex_server.library.sectionByID(media_content_id)
title = library_or_section.title
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
except KeyError as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
else:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
payload = {
"title": title,
@@ -107,6 +118,7 @@ def browse_media(
"can_play": False,
"can_expand": True,
"children": [],
"children_media_class": children_media_class,
}
method = SPECIAL_METHODS[special_folder]
@@ -116,13 +128,20 @@ def browse_media(
payload["children"].append(item_payload(item))
except UnknownMediaType:
continue
return BrowseMedia(**payload)
if media_content_type in ["server", None]:
return server_payload(plex_server)
try:
if media_content_type in ["server", None]:
return server_payload(plex_server)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
except UnknownMediaType as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
if media_content_type == "playlists":
return playlists_payload(plex_server)
@@ -160,6 +179,11 @@ def item_payload(item):
def library_section_payload(section):
"""Create response payload for a single library section."""
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
except KeyError as err:
_LOGGER.debug("Unknown type received: %s", section.TYPE)
raise UnknownMediaType from err
return BrowseMedia(
title=section.title,
media_class=MEDIA_CLASS_DIRECTORY,
@@ -167,6 +191,7 @@ def library_section_payload(section):
media_content_type="library",
can_play=False,
can_expand=True,
children_media_class=children_media_class,
)
@@ -194,6 +219,7 @@ def server_payload(plex_server):
can_expand=True,
)
server_info.children = []
server_info.children_media_class = MEDIA_CLASS_DIRECTORY
server_info.children.append(special_library_payload(server_info, "On Deck"))
server_info.children.append(special_library_payload(server_info, "Recently Added"))
for library in plex_server.library.sections():
@@ -229,4 +255,6 @@ def playlists_payload(plex_server):
playlists_info["children"].append(item_payload(playlist))
except UnknownMediaType:
continue
return BrowseMedia(**playlists_info)
response = BrowseMedia(**playlists_info)
response.children_media_class = MEDIA_CLASS_PLAYLIST
return response
@@ -0,0 +1,154 @@
"""Support for media browsing."""
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
CONTENT_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY,
}
PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_APP,
MEDIA_TYPE_CHANNEL,
]
EXPANDABLE_MEDIA_TYPES = [
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNELS,
]
def build_item_response(coordinator, payload):
"""Create response payload for the provided media query."""
search_id = payload["search_id"]
search_type = payload["search_type"]
thumbnail = None
title = None
media = None
if search_type == MEDIA_TYPE_APPS:
title = "Apps"
media = [
{"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP}
for item in coordinator.data.apps
]
elif search_type == MEDIA_TYPE_CHANNELS:
title = "Channels"
media = [
{
"channel_number": item.number,
"title": item.name,
"type": MEDIA_TYPE_CHANNEL,
}
for item in coordinator.data.channels
]
if media is None:
return None
return BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=search_id,
media_content_type=search_type,
title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True,
children=[item_payload(item, coordinator) for item in media],
thumbnail=thumbnail,
)
def item_payload(item, coordinator):
"""
Create response payload for a single media item.
Used by async_browse_media.
"""
thumbnail = None
if "app_id" in item:
media_content_type = MEDIA_TYPE_APP
media_content_id = item["app_id"]
thumbnail = coordinator.roku.app_icon_url(item["app_id"])
elif "channel_number" in item:
media_content_type = MEDIA_TYPE_CHANNEL
media_content_id = item["channel_number"]
else:
media_content_type = item["type"]
media_content_id = ""
title = item["title"]
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id
can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
return BrowseMedia(
title=title,
media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type],
media_content_type=media_content_type,
media_content_id=media_content_id,
can_play=can_play,
can_expand=can_expand,
thumbnail=thumbnail,
)
def library_payload(coordinator):
"""
Create response payload to describe contents of a specific library.
Used by async_browse_media.
"""
library_info = BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
title="Media Library",
can_play=False,
can_expand=True,
children=[],
)
library = {
MEDIA_TYPE_APPS: "Apps",
MEDIA_TYPE_CHANNELS: "Channels",
}
for item in [{"title": name, "type": type_} for type_, name in library.items()]:
if (
item["type"] == MEDIA_TYPE_CHANNELS
and coordinator.data.info.device_type != "tv"
):
continue
library_info.children.append(
item_payload(
{"title": item["title"], "type": item["type"]},
coordinator,
)
)
if all(
child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_APP
elif all(
child.media_content_type == MEDIA_TYPE_CHANNELS
for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_CHANNEL
else:
library_info.children_media_class = MEDIA_CLASS_DIRECTORY
return library_info
+7 -90
View File
@@ -7,17 +7,11 @@ import voluptuous as vol
from homeassistant.components.media_player import (
DEVICE_CLASS_RECEIVER,
DEVICE_CLASS_TV,
BrowseMedia,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
@@ -42,6 +36,7 @@ from homeassistant.const import (
from homeassistant.helpers import entity_platform
from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler
from .browse_media import build_item_response, library_payload
from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH
_LOGGER = logging.getLogger(__name__)
@@ -78,44 +73,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
def browse_media_library(channels: bool = False) -> BrowseMedia:
"""Create response payload to describe contents of a specific library."""
library_info = BrowseMedia(
title="Media Library",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
can_play=False,
can_expand=True,
children=[],
)
library_info.children.append(
BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APP,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
can_play=False,
)
)
if channels:
library_info.children.append(
BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNEL,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
can_play=False,
)
)
return library_info
class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
"""Representation of a Roku media player on the network."""
@@ -284,53 +241,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
if media_content_type in [None, "library"]:
is_tv = self.coordinator.data.info.device_type == "tv"
return browse_media_library(channels=is_tv)
return library_payload(self.coordinator)
response = None
if media_content_type == MEDIA_TYPE_APPS:
response = BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APP,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
can_play=False,
children=[
BrowseMedia(
title=app.name,
thumbnail=self.coordinator.roku.app_icon_url(app.app_id),
media_class=MEDIA_CLASS_APP,
media_content_id=app.app_id,
media_content_type=MEDIA_TYPE_APP,
can_play=True,
can_expand=False,
)
for app in self.coordinator.data.apps
],
)
if media_content_type == MEDIA_TYPE_CHANNELS:
response = BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNEL,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
can_play=False,
children=[
BrowseMedia(
title=channel.name,
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=channel.number,
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,
can_expand=False,
)
for channel in self.coordinator.data.channels
],
)
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
response = build_item_response(self.coordinator, payload)
if response is None:
raise BrowseError(
+4 -1
View File
@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_MODE,
CONF_SEQUENCE,
CONF_VARIABLES,
SERVICE_RELOAD,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
@@ -59,6 +60,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema(
vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_FIELDS, default={}): {
cv.string: {
vol.Optional(CONF_DESCRIPTION): cv.string,
@@ -75,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema(
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema(
{vol.Optional(ATTR_VARIABLES): dict}
{vol.Optional(ATTR_VARIABLES): {str: cv.match_all}}
)
RELOAD_SERVICE_SCHEMA = vol.Schema({})
@@ -263,6 +265,7 @@ class ScriptEntity(ToggleEntity):
max_runs=cfg[CONF_MAX],
max_exceeded=cfg[CONF_MAX_EXCEEDED],
logger=logging.getLogger(f"{__name__}.{object_id}"),
variables=cfg.get(CONF_VARIABLES),
)
self._changed = asyncio.Event()
@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.3.0"],
"zeroconf": ["_http._tcp.local."],
"requirements": ["aioshelly==0.3.1"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu"]
}
+6 -2
View File
@@ -1,5 +1,5 @@
"""Switch for Shelly."""
from aioshelly import RelayBlock
from aioshelly import Block
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
@@ -13,6 +13,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up switches for device."""
wrapper = hass.data[DOMAIN][config_entry.entry_id]
# In roller mode the relay blocks exist but do not contain required info
if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay":
return
relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"]
if not relay_blocks:
@@ -24,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class RelaySwitch(ShellyBlockEntity, SwitchEntity):
"""Switch that controls a relay block on Shelly devices."""
def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None:
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize relay switch."""
super().__init__(wrapper, block)
self.control_result = None
@@ -5,12 +5,13 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
"pysmappee==0.2.10"
"pysmappee==0.2.13"
],
"codeowners": [
"@bsmappee"
],
"zeroconf": [
"_ssh._tcp.local."
{"type":"_ssh._tcp.local.", "name":"smappee1*"},
{"type":"_ssh._tcp.local.", "name":"smappee2*"}
]
}
+31 -30
View File
@@ -1,34 +1,35 @@
{
"config": {
"flow_title": "Smappee: {name}",
"step": {
"environment": {
"description": "Set up your Smappee to integrate with Home Assistant.",
"data": {
"environment": "Environment"
}
},
"local": {
"description": "Enter the host to initiate the Smappee local integration",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
"title": "Discovered Smappee device"
},
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
"authorize_url_timeout": "Timeout generating authorize url.",
"connection_error": "Failed to connect to Smappee device.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"invalid_mdns": "Unsupported device for the Smappee integration."
"config": {
"flow_title": "Smappee: {name}",
"step": {
"environment": {
"description": "Set up your Smappee to integrate with Home Assistant.",
"data": {
"environment": "Environment"
}
},
"local": {
"description": "Enter the host to initiate the Smappee local integration",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
"title": "Discovered Smappee device"
},
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
"authorize_url_timeout": "Timeout generating authorize url.",
"connection_error": "Failed to connect to Smappee device.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"invalid_mdns": "Unsupported device for the Smappee integration.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
}
}
+2 -1
View File
@@ -6,7 +6,8 @@
"abort": {
"already_setup": "You can only configure one Somfy account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Somfy component is not configured. Please follow the documentation."
"missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": { "default": "Successfully authenticated with Somfy." }
}
+35 -5
View File
@@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light"
UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None}
class UnknownMediaType(BrowseError):
"""Unknown media type."""
class SonosData:
"""Storage class for platform global data."""
@@ -1487,7 +1491,20 @@ def build_item_response(media_library, payload):
except IndexError:
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]]
try:
media_class = SONOS_TO_MEDIA_CLASSES[
MEDIA_TYPES_TO_SONOS[payload["search_type"]]
]
except KeyError:
_LOGGER.debug("Unknown media type received %s", payload["search_type"])
return None
children = []
for item in media:
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title=title,
@@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload):
media_class=media_class,
media_content_id=payload["idstring"],
media_content_type=payload["search_type"],
children=[item_payload(item) for item in media],
children=children,
can_play=can_play(payload["search_type"]),
can_expand=can_expand(payload["search_type"]),
)
@@ -1507,12 +1524,18 @@ def item_payload(item):
Used by async_browse_media.
"""
media_type = get_media_type(item)
try:
media_class = SONOS_TO_MEDIA_CLASSES[media_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received %s", media_type)
raise UnknownMediaType from err
return BrowseMedia(
title=item.title,
thumbnail=getattr(item, "album_art_uri", None),
media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)],
media_class=media_class,
media_content_id=get_content_id(item),
media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)],
media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
can_play=can_play(item.item_class),
can_expand=can_expand(item),
)
@@ -1524,6 +1547,13 @@ def library_payload(media_library):
Used by async_browse_media.
"""
children = []
for item in media_library.browse():
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title="Music Library",
media_class=MEDIA_CLASS_DIRECTORY,
@@ -1531,7 +1561,7 @@ def library_payload(media_library):
media_content_type="library",
can_play=False,
can_expand=True,
children=[item_payload(item) for item in media_library.browse()],
children=children,
)
@@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import (
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_GENRE,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_PODCAST,
MEDIA_CLASS_TRACK,
@@ -104,24 +105,57 @@ LIBRARY_MAP = {
}
CONTENT_TYPE_MEDIA_CLASS = {
"current_user_playlists": MEDIA_CLASS_PLAYLIST,
"current_user_followed_artists": MEDIA_CLASS_ARTIST,
"current_user_saved_albums": MEDIA_CLASS_ALBUM,
"current_user_saved_tracks": MEDIA_CLASS_TRACK,
"current_user_saved_shows": MEDIA_CLASS_PODCAST,
"current_user_recently_played": MEDIA_CLASS_TRACK,
"current_user_top_artists": MEDIA_CLASS_ARTIST,
"current_user_top_tracks": MEDIA_CLASS_TRACK,
"featured_playlists": MEDIA_CLASS_PLAYLIST,
"categories": MEDIA_CLASS_DIRECTORY,
"category_playlists": MEDIA_CLASS_PLAYLIST,
"new_releases": MEDIA_CLASS_ALBUM,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST,
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
"current_user_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"current_user_followed_artists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST,
},
"current_user_saved_albums": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ALBUM,
},
"current_user_saved_tracks": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"current_user_saved_shows": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PODCAST,
},
"current_user_recently_played": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"current_user_top_artists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST,
},
"current_user_top_tracks": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"featured_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE},
"category_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM},
MEDIA_TYPE_PLAYLIST: {
"parent": MEDIA_CLASS_PLAYLIST,
"children": MEDIA_CLASS_TRACK,
},
MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK},
MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM},
MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None},
MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE},
MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None},
}
@@ -542,7 +576,8 @@ def build_item_response(spotify, user, payload):
if media_content_type == "categories":
media_item = BrowseMedia(
title=LIBRARY_MAP.get(media_content_id),
media_class=media_class,
media_class=media_class["parent"],
children_media_class=media_class["children"],
media_content_id=media_content_id,
media_content_type=media_content_type,
can_play=False,
@@ -559,6 +594,7 @@ def build_item_response(spotify, user, payload):
BrowseMedia(
title=item.get("name"),
media_class=MEDIA_CLASS_PLAYLIST,
children_media_class=MEDIA_CLASS_TRACK,
media_content_id=item_id,
media_content_type="category_playlists",
thumbnail=fetch_image_url(item, key="icons"),
@@ -566,6 +602,7 @@ def build_item_response(spotify, user, payload):
can_expand=True,
)
)
return media_item
if title is None:
if "name" in media:
@@ -573,9 +610,10 @@ def build_item_response(spotify, user, payload):
else:
title = LIBRARY_MAP.get(payload["media_content_id"])
response = {
params = {
"title": title,
"media_class": media_class,
"media_class": media_class["parent"],
"children_media_class": media_class["children"],
"media_content_id": media_content_id,
"media_content_type": media_content_type,
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
@@ -584,16 +622,16 @@ def build_item_response(spotify, user, payload):
}
for item in items:
try:
response["children"].append(item_payload(item))
params["children"].append(item_payload(item))
except (MissingMediaInformation, UnknownMediaType):
continue
if "images" in media:
response["thumbnail"] = fetch_image_url(media)
params["thumbnail"] = fetch_image_url(media)
elif image:
response["thumbnail"] = image
params["thumbnail"] = image
return BrowseMedia(**response)
return BrowseMedia(**params)
def item_payload(item):
@@ -622,17 +660,14 @@ def item_payload(item):
payload = {
"title": item.get("name"),
"media_class": media_class["parent"],
"children_media_class": media_class["children"],
"media_content_id": media_id,
"media_content_type": media_type,
"can_play": media_type in PLAYABLE_MEDIA_TYPES,
"can_expand": can_expand,
}
payload = {
**payload,
"media_class": media_class,
}
if "images" in item:
payload["thumbnail"] = fetch_image_url(item)
elif MEDIA_TYPE_ALBUM in item:
@@ -663,7 +698,9 @@ def library_payload():
{"name": item["name"], "type": item["type"], "uri": item["type"]}
)
)
return BrowseMedia(**library_info)
response = BrowseMedia(**library_info)
response.children_media_class = MEDIA_CLASS_DIRECTORY
return response
def fetch_image_url(item, key="images"):
@@ -10,6 +10,7 @@
"abort": {
"already_setup": "You can only configure one Spotify account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
},
+10 -2
View File
@@ -25,7 +25,10 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence):
segment,
mode="w",
format=stream_output.format,
container_options=container_options,
container_options={
"video_track_timescale": str(int(1 / video_stream.time_base)),
**container_options,
},
)
vstream = output.add_stream(template=video_stream)
# Check if audio is requested
@@ -64,11 +67,16 @@ def _stream_worker_internal(hass, stream, quit_event):
video_stream = container.streams.video[0]
except (KeyError, IndexError):
_LOGGER.error("Stream has no video")
container.close()
return
try:
audio_stream = container.streams.audio[0]
except (KeyError, IndexError):
audio_stream = None
# These formats need aac_adtstoasc bitstream filter, but auto_bsf not
# compatible with empty_moov and manual bitstream filters not in PyAV
if container.format.name in {"hls", "mpegts"}:
audio_stream = None
# The presentation timestamps of the first packet in each stream we receive
# Use to adjust before muxing or outputting, but we don't adjust internally
@@ -238,7 +246,7 @@ def _stream_worker_internal(hass, stream, quit_event):
# Update last_dts processed
last_dts[packet.stream] = packet.dts
# mux video packets immediately, save audio packets to be muxed all at once
# mux packets
if packet.stream == video_stream:
mux_video_packet(packet) # mutates packet timestamps
else:
@@ -5,6 +5,7 @@ from typing import Any, Callable, List, Optional, Union
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
@@ -129,6 +130,7 @@ class TemplateEntity(Entity):
self._entity_picture_template = entity_picture_template
self._icon = None
self._entity_picture = None
self._self_ref_update_count = 0
@property
def should_poll(self):
@@ -222,9 +224,28 @@ class TemplateEntity(Entity):
updates: List[TrackTemplateResult],
) -> None:
"""Call back the results to the attributes."""
if event:
self.async_set_context(event.context)
entity_id = event and event.data.get(ATTR_ENTITY_ID)
if entity_id and entity_id == self.entity_id:
self._self_ref_update_count += 1
else:
self._self_ref_update_count = 0
# If we need to make this less sensitive in the future,
# change the '>=' to a '>' here.
if self._self_ref_update_count >= len(self._template_attrs):
for update in updates:
_LOGGER.warning(
"Template loop detected while processing event: %s, skipping template render for Template[%s]",
event,
update.template.template,
)
return
for update in updates:
for attr in self._template_attrs[update.template]:
attr.handle_result(
+2 -1
View File
@@ -17,7 +17,8 @@
"authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_agreements": "This account has no Toon displays."
"no_agreements": "This account has no Toon displays.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
}
}
@@ -253,9 +253,11 @@ def handle_render_template(hass, connection, msg):
template.hass = hass
variables = msg.get("variables")
info = None
@callback
def _template_listener(event, updates):
nonlocal info
track_template_result = updates.pop()
result = track_template_result.result
if isinstance(result, TemplateError):
@@ -267,7 +269,11 @@ def handle_render_template(hass, connection, msg):
result = None
connection.send_message(messages.event_message(msg["id"], {"result": result}))
connection.send_message(
messages.event_message(
msg["id"], {"result": result, "listeners": info.listeners} # type: ignore
)
)
info = async_track_template_result(
hass, [TrackTemplate(template, variables)], _template_listener
@@ -19,7 +19,8 @@
"abort": {
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Withings integration is not configured. Please follow the documentation.",
"already_configured": "Configuration updated for profile."
"already_configured": "Configuration updated for profile.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": { "default": "Successfully authenticated with Withings." }
}
+19 -2
View File
@@ -1,5 +1,6 @@
"""Support for exposing Home Assistant via Zeroconf."""
import asyncio
import fnmatch
import ipaddress
import logging
import socket
@@ -268,10 +269,26 @@ def setup(hass, config):
# likely bad homekit data
return
for domain in zeroconf_types[service_type]:
for entry in zeroconf_types[service_type]:
if len(entry) > 1:
if "macaddress" in entry:
if "properties" not in info:
continue
if "macaddress" not in info["properties"]:
continue
if not fnmatch.fnmatch(
info["properties"]["macaddress"], entry["macaddress"]
):
continue
if "name" in entry:
if "name" not in info:
continue
if not fnmatch.fnmatch(info["name"], entry["name"]):
continue
hass.add_job(
hass.config_entries.flow.async_init(
domain, context={"source": DOMAIN}, data=info
entry["domain"], context={"source": DOMAIN}, data=info
)
)
@@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.28.4"],
"requirements": ["zeroconf==0.28.5"],
"dependencies": ["api"],
"codeowners": ["@Kane610"],
"quality_scale": "internal"
+2 -1
View File
@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 115
PATCH_VERSION = "0b4"
PATCH_VERSION = "0b7"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)
@@ -179,6 +179,7 @@ CONF_UNTIL = "until"
CONF_URL = "url"
CONF_USERNAME = "username"
CONF_VALUE_TEMPLATE = "value_template"
CONF_VARIABLES = "variables"
CONF_VERIFY_SSL = "verify_ssl"
CONF_WAIT_FOR_TRIGGER = "wait_for_trigger"
CONF_WAIT_TEMPLATE = "wait_template"
+89 -24
View File
@@ -7,72 +7,137 @@ To update, run python3 -m script.hassfest
ZEROCONF = {
"_Volumio._tcp.local.": [
"volumio"
{
"domain": "volumio"
}
],
"_api._udp.local.": [
"guardian"
{
"domain": "guardian"
}
],
"_axis-video._tcp.local.": [
"axis",
"doorbird"
{
"domain": "axis",
"macaddress": "00408C*"
},
{
"domain": "axis",
"macaddress": "ACCC8E*"
},
{
"domain": "axis",
"macaddress": "B8A44F*"
},
{
"domain": "doorbird",
"macaddress": "1CCAE3*"
}
],
"_bond._tcp.local.": [
"bond"
{
"domain": "bond"
}
],
"_daap._tcp.local.": [
"forked_daapd"
{
"domain": "forked_daapd"
}
],
"_dkapi._tcp.local.": [
"daikin"
{
"domain": "daikin"
}
],
"_elg._tcp.local.": [
"elgato"
{
"domain": "elgato"
}
],
"_esphomelib._tcp.local.": [
"esphome"
{
"domain": "esphome"
}
],
"_googlecast._tcp.local.": [
"cast"
{
"domain": "cast"
}
],
"_hap._tcp.local.": [
"homekit_controller"
{
"domain": "homekit_controller"
}
],
"_http._tcp.local.": [
"shelly"
{
"domain": "shelly",
"name": "shelly*"
}
],
"_ipp._tcp.local.": [
"ipp"
{
"domain": "ipp"
}
],
"_ipps._tcp.local.": [
"ipp"
{
"domain": "ipp"
}
],
"_miio._udp.local.": [
"xiaomi_aqara",
"xiaomi_miio"
{
"domain": "xiaomi_aqara"
},
{
"domain": "xiaomi_miio"
}
],
"_nut._tcp.local.": [
"nut"
{
"domain": "nut"
}
],
"_plugwise._tcp.local.": [
"plugwise"
{
"domain": "plugwise"
}
],
"_printer._tcp.local.": [
"brother"
{
"domain": "brother",
"name": "brother*"
}
],
"_spotify-connect._tcp.local.": [
"spotify"
{
"domain": "spotify"
}
],
"_ssh._tcp.local.": [
"smappee"
{
"domain": "smappee",
"name": "smappee1*"
},
{
"domain": "smappee",
"name": "smappee2*"
}
],
"_viziocast._tcp.local.": [
"vizio"
{
"domain": "vizio"
}
],
"_wled._tcp.local.": [
"wled"
{
"domain": "wled"
}
],
"_xbmc-jsonrpc-h._tcp.local.": [
"kodi"
{
"domain": "kodi"
}
]
}
@@ -21,7 +21,7 @@ from yarl import URL
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.network import get_url
from homeassistant.helpers.network import NoURLAvailableError, get_url
from .aiohttp_client import async_get_clientsession
@@ -251,6 +251,13 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
url = await self.flow_impl.async_generate_authorize_url(self.flow_id)
except asyncio.TimeoutError:
return self.async_abort(reason="authorize_url_timeout")
except NoURLAvailableError:
return self.async_abort(
reason="no_url_available",
description_placeholders={
"docs_url": "https://www.home-assistant.io/more-info/no-url-available"
},
)
url = str(URL(url).update_query(self.extra_authorize_data))
+24 -1
View File
@@ -67,6 +67,7 @@ from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC,
CONF_UNTIL,
CONF_VALUE_TEMPLATE,
CONF_VARIABLES,
CONF_WAIT_FOR_TRIGGER,
CONF_WAIT_TEMPLATE,
CONF_WHILE,
@@ -81,7 +82,10 @@ from homeassistant.const import (
)
from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template as template_helper
from homeassistant.helpers import (
script_variables as script_variables_helper,
template as template_helper,
)
from homeassistant.helpers.logging import KeywordStyleAdapter
from homeassistant.util import slugify as util_slugify
import homeassistant.util.dt as dt_util
@@ -863,6 +867,13 @@ def make_entity_service_schema(
)
SCRIPT_VARIABLES_SCHEMA = vol.All(
vol.Schema({str: template_complex}),
# pylint: disable=unnecessary-lambda
lambda val: script_variables_helper.ScriptVariables(val),
)
def script_action(value: Any) -> dict:
"""Validate a script action."""
if not isinstance(value, dict):
@@ -1117,6 +1128,13 @@ _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema(
}
)
_SCRIPT_SET_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ALIAS): string,
vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
}
)
SCRIPT_ACTION_DELAY = "delay"
SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template"
SCRIPT_ACTION_CHECK_CONDITION = "condition"
@@ -1127,6 +1145,7 @@ SCRIPT_ACTION_ACTIVATE_SCENE = "scene"
SCRIPT_ACTION_REPEAT = "repeat"
SCRIPT_ACTION_CHOOSE = "choose"
SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger"
SCRIPT_ACTION_VARIABLES = "variables"
def determine_script_action(action: dict) -> str:
@@ -1158,6 +1177,9 @@ def determine_script_action(action: dict) -> str:
if CONF_WAIT_FOR_TRIGGER in action:
return SCRIPT_ACTION_WAIT_FOR_TRIGGER
if CONF_VARIABLES in action:
return SCRIPT_ACTION_VARIABLES
return SCRIPT_ACTION_CALL_SERVICE
@@ -1172,4 +1194,5 @@ ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = {
SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA,
SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA,
SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA,
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
}
+9
View File
@@ -526,6 +526,15 @@ class _TrackTemplateResultInfo:
self._last_info = self._info.copy()
self._create_listeners()
@property
def listeners(self) -> Dict:
"""State changes that will cause a re-render."""
return {
"all": self._all_listener is not None,
"entities": self._last_entities,
"domains": self._last_domains,
}
@property
def _needs_all_listener(self) -> bool:
for track_template_ in self._track_templates:
+32
View File
@@ -75,6 +75,38 @@ def get_url(
except NoURLAvailableError:
pass
# For current request, we accept loopback interfaces (e.g., 127.0.0.1),
# the Supervisor hostname and localhost transparently
request_host = _get_request_host()
if (
require_current_request
and request_host is not None
and hass.config.api is not None
):
scheme = "https" if hass.config.api.use_ssl else "http"
current_url = yarl.URL.build(
scheme=scheme, host=request_host, port=hass.config.api.port
)
known_hostname = None
if hass.components.hassio.is_hassio():
host_info = hass.components.hassio.get_host_info()
known_hostname = f"{host_info['hostname']}.local"
if (
(
(
allow_ip
and is_ip_address(request_host)
and is_loopback(ip_address(request_host))
)
or request_host in ["localhost", known_hostname]
)
and (not require_ssl or current_url.scheme == "https")
and (not require_standard_port or current_url.is_default_port())
):
return normalize_url(str(current_url))
# We have to be honest now, we have no viable option available
raise NoURLAvailableError
+33 -7
View File
@@ -46,6 +46,7 @@ from homeassistant.const import (
CONF_SEQUENCE,
CONF_TIMEOUT,
CONF_UNTIL,
CONF_VARIABLES,
CONF_WAIT_FOR_TRIGGER,
CONF_WAIT_TEMPLATE,
CONF_WHILE,
@@ -53,12 +54,9 @@ from homeassistant.const import (
SERVICE_TURN_ON,
)
from homeassistant.core import SERVICE_CALL_LIMIT, Context, HomeAssistant, callback
from homeassistant.helpers import (
condition,
config_validation as cv,
template as template,
)
from homeassistant.helpers import condition, config_validation as cv, template
from homeassistant.helpers.event import async_call_later, async_track_template
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import (
CONF_SERVICE_DATA,
async_prepare_call_from_config,
@@ -615,6 +613,14 @@ class _ScriptRun:
task.cancel()
remove_triggers()
async def _async_variables_step(self):
"""Set a variable value."""
self._script.last_action = self._action.get(CONF_ALIAS, "setting variables")
self._log("Executing step %s", self._script.last_action)
self._variables = self._action[CONF_VARIABLES].async_render(
self._hass, self._variables, render_as_defaults=False
)
async def _async_run_script(self, script):
"""Execute a script."""
await self._async_run_long_action(
@@ -721,6 +727,7 @@ class Script:
logger: Optional[logging.Logger] = None,
log_exceptions: bool = True,
top_level: bool = True,
variables: Optional[ScriptVariables] = None,
) -> None:
"""Initialize the script."""
all_scripts = hass.data.get(DATA_SCRIPTS)
@@ -759,6 +766,10 @@ class Script:
self._choose_data: Dict[int, Dict[str, Any]] = {}
self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None
self.variables = variables
self._variables_dynamic = template.is_complex(variables)
if self._variables_dynamic:
template.attach(hass, variables)
def _set_logger(self, logger: Optional[logging.Logger] = None) -> None:
if logger:
@@ -867,7 +878,7 @@ class Script:
async def async_run(
self,
variables: Optional[_VarsType] = None,
run_variables: Optional[_VarsType] = None,
context: Optional[Context] = None,
started_action: Optional[Callable[..., Any]] = None,
) -> None:
@@ -898,8 +909,23 @@ class Script:
# are read-only, but more importantly, so as not to leak any variables created
# during the run back to the caller.
if self._top_level:
variables = dict(variables) if variables is not None else {}
if self.variables:
try:
variables = self.variables.async_render(
self._hass,
run_variables,
)
except template.TemplateError as err:
self._log("Error rendering variables: %s", err, level=logging.ERROR)
raise
elif run_variables:
variables = dict(run_variables)
else:
variables = {}
variables["context"] = context
else:
variables = cast(dict, run_variables)
if self.script_mode != SCRIPT_MODE_QUEUED:
cls = _ScriptRun
+64
View File
@@ -0,0 +1,64 @@
"""Script variables."""
from typing import Any, Dict, Mapping, Optional
from homeassistant.core import HomeAssistant, callback
from . import template
class ScriptVariables:
"""Class to hold and render script variables."""
def __init__(self, variables: Dict[str, Any]):
"""Initialize script variables."""
self.variables = variables
self._has_template: Optional[bool] = None
@callback
def async_render(
self,
hass: HomeAssistant,
run_variables: Optional[Mapping[str, Any]],
*,
render_as_defaults: bool = True,
) -> Dict[str, Any]:
"""Render script variables.
The run variables are used to compute the static variables.
If `render_as_defaults` is True, the run variables will not be overridden.
"""
if self._has_template is None:
self._has_template = template.is_complex(self.variables)
template.attach(hass, self.variables)
if not self._has_template:
if render_as_defaults:
rendered_variables = dict(self.variables)
if run_variables is not None:
rendered_variables.update(run_variables)
else:
rendered_variables = (
{} if run_variables is None else dict(run_variables)
)
rendered_variables.update(self.variables)
return rendered_variables
rendered_variables = {} if run_variables is None else dict(run_variables)
for key, value in self.variables.items():
# We can skip if we're going to override this key with
# run variables anyway
if render_as_defaults and key in rendered_variables:
continue
rendered_variables[key] = template.render_complex(value, rendered_variables)
return rendered_variables
def as_dict(self) -> dict:
"""Return dict version of this class."""
return self.variables
+15 -2
View File
@@ -65,7 +65,7 @@ def attach(hass: HomeAssistantType, obj: Any) -> None:
if isinstance(obj, list):
for child in obj:
attach(hass, child)
elif isinstance(obj, dict):
elif isinstance(obj, collections.abc.Mapping):
for child_key, child_value in obj.items():
attach(hass, child_key)
attach(hass, child_value)
@@ -77,7 +77,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any:
"""Recursive template creator helper function."""
if isinstance(value, list):
return [render_complex(item, variables) for item in value]
if isinstance(value, dict):
if isinstance(value, collections.abc.Mapping):
return {
render_complex(key, variables): render_complex(item, variables)
for key, item in value.items()
@@ -88,6 +88,19 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any:
return value
def is_complex(value: Any) -> bool:
"""Test if data structure is a complex template."""
if isinstance(value, Template):
return True
if isinstance(value, list):
return any(is_complex(val) for val in value)
if isinstance(value, collections.abc.Mapping):
return any(is_complex(val) for val in value.keys()) or any(
is_complex(val) for val in value.values()
)
return False
def is_template_string(maybe_template: str) -> bool:
"""Check if the input is a Jinja2 template."""
return _RE_JINJA_DELIMITERS.search(maybe_template) is not None
+2 -2
View File
@@ -1,5 +1,5 @@
"""Typing Helpers for Home Assistant."""
from typing import Any, Dict, Optional, Tuple, Union
from typing import Any, Dict, Mapping, Optional, Tuple, Union
import homeassistant.core
@@ -12,7 +12,7 @@ HomeAssistantType = homeassistant.core.HomeAssistant
ServiceCallType = homeassistant.core.ServiceCall
ServiceDataType = Dict[str, Any]
StateType = Union[None, str, int, float]
TemplateVarsType = Optional[Dict[str, Any]]
TemplateVarsType = Optional[Mapping[str, Any]]
# Custom type for recorder Queries
QueryType = Any
+13 -6
View File
@@ -145,18 +145,25 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]:
return flows
async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List]:
async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]:
"""Return cached list of zeroconf types."""
zeroconf: Dict[str, List] = ZEROCONF.copy()
zeroconf: Dict[str, List[Dict[str, str]]] = ZEROCONF.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
if not integration.zeroconf:
continue
for typ in integration.zeroconf:
zeroconf.setdefault(typ, [])
if integration.domain not in zeroconf[typ]:
zeroconf[typ].append(integration.domain)
for entry in integration.zeroconf:
data = {"domain": integration.domain}
if isinstance(entry, dict):
typ = entry["type"]
entry_without_type = entry.copy()
del entry_without_type["type"]
data.update(entry_without_type)
else:
typ = entry
zeroconf.setdefault(typ, []).append(data)
return zeroconf
+3 -3
View File
@@ -12,8 +12,8 @@ cryptography==2.9.2
defusedxml==0.6.0
distro==1.5.0
emoji==0.5.4
hass-nabucasa==0.36.1
home-assistant-frontend==20200908.0
hass-nabucasa==0.37.0
home-assistant-frontend==20200909.0
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
netdisco==2.8.2
@@ -29,7 +29,7 @@ sqlalchemy==1.3.19
voluptuous-serialize==2.4.0
voluptuous==0.11.7
yarl==1.4.2
zeroconf==0.28.4
zeroconf==0.28.5
pycryptodome>=3.6.6
+2 -1
View File
@@ -53,7 +53,8 @@
"already_configured_device": "Device is already configured",
"no_devices_found": "No devices found on the network",
"oauth2_missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth2_authorize_url_timeout": "Timeout generating authorize URL."
"oauth2_authorize_url_timeout": "Timeout generating authorize URL.",
"oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
}
}
}
+7 -7
View File
@@ -221,7 +221,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
aioshelly==0.3.0
aioshelly==0.3.1
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -720,7 +720,7 @@ habitipy==0.2.0
hangups==0.4.10
# homeassistant.components.cloud
hass-nabucasa==0.36.1
hass-nabucasa==0.37.0
# homeassistant.components.jewish_calendar
hdate==0.9.5
@@ -747,7 +747,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20200908.0
home-assistant-frontend==20200909.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -1431,7 +1431,7 @@ pyitachip2ir==0.0.7
pykira==0.1.1
# homeassistant.components.kodi
pykodi==0.1.2
pykodi==0.2.0
# homeassistant.components.kwb
pykwb==0.0.8
@@ -1455,7 +1455,7 @@ pylibrespot-java==0.1.0
pylitejet==0.1
# homeassistant.components.loopenergy
pyloopenergy==0.1.3
pyloopenergy==0.2.1
# homeassistant.components.lutron_caseta
pylutron-caseta==0.6.1
@@ -1632,7 +1632,7 @@ pyskyqhub==0.1.3
pysma==0.3.5
# homeassistant.components.smappee
pysmappee==0.2.10
pysmappee==0.2.13
# homeassistant.components.smartthings
pysmartapp==0.3.2
@@ -2287,7 +2287,7 @@ youtube_dl==2020.07.28
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.28.4
zeroconf==0.28.5
# homeassistant.components.zha
zha-quirks==0.0.44
+6 -6
View File
@@ -131,7 +131,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.shelly
aioshelly==0.3.0
aioshelly==0.3.1
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -352,7 +352,7 @@ ha-ffmpeg==2.0
hangups==0.4.10
# homeassistant.components.cloud
hass-nabucasa==0.36.1
hass-nabucasa==0.37.0
# homeassistant.components.jewish_calendar
hdate==0.9.5
@@ -370,7 +370,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20200908.0
home-assistant-frontend==20200909.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -692,7 +692,7 @@ pyisy==2.0.2
pykira==0.1.1
# homeassistant.components.kodi
pykodi==0.1.2
pykodi==0.2.0
# homeassistant.components.lastfm
pylast==3.3.0
@@ -782,7 +782,7 @@ pysignalclirestapi==0.3.4
pysma==0.3.5
# homeassistant.components.smappee
pysmappee==0.2.10
pysmappee==0.2.13
# homeassistant.components.smartthings
pysmartapp==0.3.2
@@ -1053,7 +1053,7 @@ xmltodict==0.12.0
yeelight==0.5.3
# homeassistant.components.zeroconf
zeroconf==0.28.4
zeroconf==0.28.5
# homeassistant.components.zha
zha-quirks==0.0.44
+1 -1
View File
@@ -8,4 +8,4 @@ cd "$(dirname "$0")/.."
echo "Installing development dependencies..."
python3 -m pip install wheel --constraint homeassistant/package_constraints.txt
python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) --constraint homeassistant/package_constraints.txt
python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt
+12 -1
View File
@@ -38,7 +38,18 @@ MANIFEST_SCHEMA = vol.Schema(
vol.Required("domain"): str,
vol.Required("name"): str,
vol.Optional("config_flow"): bool,
vol.Optional("zeroconf"): [str],
vol.Optional("zeroconf"): [
vol.Any(
str,
vol.Schema(
{
vol.Required("type"): str,
vol.Optional("macaddress"): str,
vol.Optional("name"): str,
}
),
)
],
vol.Optional("ssdp"): vol.Schema(
vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))])
),
+11 -2
View File
@@ -37,8 +37,17 @@ def generate_and_validate(integrations: Dict[str, Integration]):
if not (service_types or homekit_models):
continue
for service_type in service_types:
service_type_dict[service_type].append(domain)
for entry in service_types:
data = {"domain": domain}
if isinstance(entry, dict):
typ = entry["type"]
entry_without_type = entry.copy()
del entry_without_type["type"]
data.update(entry_without_type)
else:
typ = entry
service_type_dict[typ].append(data)
for model in homekit_models:
if model in homekit_dict:
+73
View File
@@ -1134,3 +1134,76 @@ async def test_logbook_humanify_automation_triggered_event(hass):
assert event2["domain"] == "automation"
assert event2["message"] == "has been triggered by source of trigger"
assert event2["entity_id"] == "automation.bye"
async def test_automation_variables(hass, caplog):
"""Test automation variables."""
calls = async_mock_service(hass, "test", "automation")
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"variables": {
"test_var": "defined_in_config",
"event_type": "{{ trigger.event.event_type }}",
},
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {
"service": "test.automation",
"data": {
"value": "{{ test_var }}",
"event_type": "{{ event_type }}",
},
},
},
{
"variables": {
"test_var": "defined_in_config",
},
"trigger": {"platform": "event", "event_type": "test_event_2"},
"condition": {
"condition": "template",
"value_template": "{{ trigger.event.data.pass_condition }}",
},
"action": {
"service": "test.automation",
},
},
{
"variables": {
"test_var": "{{ trigger.event.data.break + 1 }}",
},
"trigger": {"platform": "event", "event_type": "test_event_3"},
"action": {
"service": "test.automation",
},
},
]
},
)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["value"] == "defined_in_config"
assert calls[0].data["event_type"] == "test_event"
hass.bus.async_fire("test_event_2")
await hass.async_block_till_done()
assert len(calls) == 1
hass.bus.async_fire("test_event_2", {"pass_condition": True})
await hass.async_block_till_done()
assert len(calls) == 2
assert "Error rendering variables" not in caplog.text
hass.bus.async_fire("test_event_3")
await hass.async_block_till_done()
assert len(calls) == 2
assert "Error rendering variables" in caplog.text
hass.bus.async_fire("test_event_3", {"break": 0})
await hass.async_block_till_done()
assert len(calls) == 3
@@ -84,17 +84,27 @@ async def test_if_fires_on_event_with_data(hass, calls):
"trigger": {
"platform": "event",
"event_type": "test_event",
"event_data": {"some_attr": "some_value"},
"event_data": {
"some_attr": "some_value",
"second_attr": "second_value",
},
},
"action": {"service": "test.automation"},
}
},
)
hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"})
hass.bus.async_fire(
"test_event",
{"some_attr": "some_value", "another": "value", "second_attr": "second_value"},
)
await hass.async_block_till_done()
assert len(calls) == 1
hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"})
await hass.async_block_till_done()
assert len(calls) == 1 # No new call
async def test_if_fires_on_event_with_empty_data_config(hass, calls):
"""Test the firing of events with empty data config.
@@ -1112,6 +1112,42 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls
assert len(calls) == 1
async def test_attribute_if_fires_on_entity_where_attr_stays_constant(hass, calls):
"""Test for firing if attribute stays the same."""
hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "state",
"entity_id": "test.entity",
"attribute": "name",
},
"action": {"service": "test.automation"},
}
},
)
await hass.async_block_till_done()
# Leave all attributes the same
hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"})
await hass.async_block_till_done()
assert len(calls) == 0
# Change the untracked attribute
hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "new_value"})
await hass.async_block_till_done()
assert len(calls) == 0
# Change the tracked attribute
hass.states.async_set("test.entity", "bla", {"name": "world", "other": "old_value"})
await hass.async_block_till_done()
assert len(calls) == 1
async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop(
hass, calls
):
+92 -1
View File
@@ -807,6 +807,91 @@ async def test_homekit_finds_linked_batteries(
)
async def test_homekit_async_get_integration_fails(
hass, hk_driver, debounce_patcher, device_reg, entity_reg
):
"""Test that we continue if async_get_integration fails."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"light.demo": {}},
DEFAULT_SAFE_MODE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit.driver = hk_driver
# pylint: disable=protected-access
homekit._filter = Mock(return_value=True)
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
sw_version="0.16.0",
model="Powerwall 2",
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
binary_charging_sensor = entity_reg.async_get_or_create(
"binary_sensor",
"invalid_integration_does_not_exist",
"battery_charging",
device_id=device_entry.id,
device_class=DEVICE_CLASS_BATTERY_CHARGING,
)
battery_sensor = entity_reg.async_get_or_create(
"sensor",
"invalid_integration_does_not_exist",
"battery",
device_id=device_entry.id,
device_class=DEVICE_CLASS_BATTERY,
)
light = entity_reg.async_get_or_create(
"light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id
)
hass.states.async_set(
binary_charging_sensor.entity_id,
STATE_ON,
{ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING},
)
hass.states.async_set(
battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}
)
hass.states.async_set(light.entity_id, STATE_ON)
def _mock_get_accessory(*args, **kwargs):
return [None, "acc", None]
with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start_service"
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
hk_driver,
ANY,
ANY,
{
"model": "Powerwall 2",
"sw_version": "0.16.0",
"platform": "invalid_integration_does_not_exist",
"linked_battery_charging_sensor": "binary_sensor.invalid_integration_does_not_exist_battery_charging",
"linked_battery_sensor": "sensor.invalid_integration_does_not_exist_battery",
},
)
async def test_setup_imported(hass):
"""Test async_setup with imported config options."""
legacy_persist_file_path = hass.config.path(HOMEKIT_FILE)
@@ -1222,7 +1307,13 @@ async def test_reload(hass):
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch(
f"{PATH_HOMEKIT}.HomeKit"
) as mock_homekit2:
) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(
f"{PATH_HOMEKIT}.get_accessory"
), patch(
"pyhap.accessory_driver.AccessoryDriver.start_service"
):
mock_homekit2.return_value = homekit = Mock()
type(homekit).async_start = AsyncMock()
await hass.services.async_call(
@@ -17,6 +17,7 @@ async def test_browse_media_as_dict():
title="media/",
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_MUSIC,
)
base.children = [
models.BrowseMediaSource(
@@ -37,6 +38,7 @@ async def test_browse_media_as_dict():
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
assert not item["can_play"]
assert item["can_expand"]
assert item["children_media_class"] == MEDIA_CLASS_MUSIC
assert len(item["children"]) == 1
assert item["children"][0]["title"] == "test.mp3"
assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC
@@ -62,6 +64,7 @@ async def test_browse_media_parent_no_children():
assert not item["can_play"]
assert item["can_expand"]
assert len(item["children"]) == 0
assert item["children_media_class"] is None
async def test_media_source_default_name():
+26 -1
View File
@@ -1,13 +1,27 @@
"""Test Met weather entity."""
from homeassistant.components.met import DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
async def test_tracking_home(hass, mock_weather):
"""Test we track home."""
await hass.config_entries.flow.async_init("met", context={"source": "onboarding"})
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 2
assert len(hass.states.async_entity_ids("weather")) == 1
assert len(mock_weather.mock_calls) == 4
# Test the hourly sensor is disabled by default
registry = await hass.helpers.entity_registry.async_get_registry()
state = hass.states.get("weather.test_home_hourly")
assert state is None
entry = registry.async_get("weather.test_home_hourly")
assert entry
assert entry.disabled
assert entry.disabled_by == "integration"
# Test we track config
await hass.config.async_update(latitude=10, longitude=20)
await hass.async_block_till_done()
@@ -21,6 +35,17 @@ async def test_tracking_home(hass, mock_weather):
async def test_not_tracking_home(hass, mock_weather):
"""Test when we not track home."""
# Pre-create registry entry for disabled by default hourly weather
registry = await hass.helpers.entity_registry.async_get_registry()
registry.async_get_or_create(
WEATHER_DOMAIN,
DOMAIN,
"10-20-hourly",
suggested_object_id="somewhere_hourly",
disabled_by=None,
)
await hass.config_entries.flow.async_init(
"met",
context={"source": "user"},
@@ -603,17 +603,71 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog):
)
async def test_discovery_update_alarm(hass, mqtt_mock, caplog):
async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog):
"""Test update of discovered alarm_control_panel."""
config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "alarm/state1"
config2["state_topic"] = "alarm/state2"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None),
]
state_data2 = [
([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None),
([("alarm/state1", '{"state2":{"state":"triggered"}}')], "armed_away", None),
([("alarm/state2", '{"state1":{"state":"triggered"}}')], "armed_away", None),
([("alarm/state2", '{"state2":{"state":"triggered"}}')], "triggered", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2
hass,
mqtt_mock,
caplog,
alarm_control_panel.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog):
"""Test update of discovered alarm_control_panel."""
config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "alarm/state1"
config2["state_topic"] = "alarm/state1"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None),
]
state_data2 = [
([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None),
([("alarm/state1", '{"state2":{"state":"triggered"}}')], "triggered", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass,
mqtt_mock,
caplog,
alarm_control_panel.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
+60 -2
View File
@@ -580,17 +580,75 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog):
)
async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog):
async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog):
"""Test update of discovered binary_sensor."""
config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "sensor/state1"
config2["state_topic"] = "sensor/state2"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None),
]
state_data2 = [
([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None),
([("sensor/state2", '{"state2":{"state":"ON"}}')], "on", None),
([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None),
([("sensor/state1", '{"state2":{"state":"OFF"}}')], "on", None),
([("sensor/state2", '{"state1":{"state":"OFF"}}')], "on", None),
([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2
hass,
mqtt_mock,
caplog,
binary_sensor.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog):
"""Test update of discovered binary_sensor."""
config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "sensor/state1"
config2["state_topic"] = "sensor/state1"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None),
]
state_data2 = [
([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None),
([("sensor/state1", '{"state2":{"state":"ON"}}')], "on", None),
([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None),
([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass,
mqtt_mock,
caplog,
binary_sensor.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
+22 -20
View File
@@ -512,10 +512,6 @@ async def help_test_discovery_update(
discovery_data2,
state_data1=None,
state_data2=None,
state1=None,
state2=None,
attributes1=None,
attributes2=None,
):
"""Test update of discovered component.
@@ -527,32 +523,38 @@ async def help_test_discovery_update(
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data1)
await hass.async_block_till_done()
if state_data1:
for (topic, data) in state_data1:
async_fire_mqtt_message(hass, topic, data)
state = hass.states.get(f"{domain}.beer")
assert state is not None
assert state.name == "Beer"
if state1:
assert state.state == state1
if attributes1:
for (attr, value) in attributes1:
assert state.attributes.get(attr) == value
if state_data1:
for (mqtt_messages, expected_state, attributes) in state_data1:
for (topic, data) in mqtt_messages:
async_fire_mqtt_message(hass, topic, data)
state = hass.states.get(f"{domain}.beer")
if expected_state:
assert state.state == expected_state
if attributes:
for (attr, value) in attributes:
assert state.attributes.get(attr) == value
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data2)
await hass.async_block_till_done()
if state_data2:
for (topic, data) in state_data2:
async_fire_mqtt_message(hass, topic, data)
state = hass.states.get(f"{domain}.beer")
assert state is not None
assert state.name == "Milk"
if state2:
assert state.state == state2
if attributes2:
for (attr, value) in attributes2:
assert state.attributes.get(attr) == value
if state_data2:
for (mqtt_messages, expected_state, attributes) in state_data2:
for (topic, data) in mqtt_messages:
async_fire_mqtt_message(hass, topic, data)
state = hass.states.get(f"{domain}.beer")
if expected_state:
assert state.state == expected_state
if attributes:
for (attr, value) in attributes:
assert state.attributes.get(attr) == value
state = hass.states.get(f"{domain}.milk")
assert state is None
+8
View File
@@ -1397,6 +1397,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock):
async def test_find_percentage_in_range_defaults(hass, mqtt_mock):
"""Test find percentage in range with default range."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1440,6 +1441,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock):
async def test_find_percentage_in_range_altered(hass, mqtt_mock):
"""Test find percentage in range with altered range."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1483,6 +1485,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock):
async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock):
"""Test find percentage in range with default range but inverted."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1526,6 +1529,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock):
async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock):
"""Test find percentage in range with altered range and inverted."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1569,6 +1573,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock):
async def test_find_in_range_defaults(hass, mqtt_mock):
"""Test find in range with default range."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1612,6 +1617,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock):
async def test_find_in_range_altered(hass, mqtt_mock):
"""Test find in range with altered range."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1655,6 +1661,7 @@ async def test_find_in_range_altered(hass, mqtt_mock):
async def test_find_in_range_defaults_inverted(hass, mqtt_mock):
"""Test find in range with default range but inverted."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
@@ -1698,6 +1705,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock):
async def test_find_in_range_altered_inverted(hass, mqtt_mock):
"""Test find in range with altered range and inverted."""
mqtt_cover = MqttCover(
hass,
{
"name": "cover.test",
"state_topic": "state-topic",
+456 -15
View File
@@ -153,6 +153,7 @@ light:
payload_off: "off"
"""
import json
from os import path
import pytest
@@ -1466,20 +1467,249 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog):
assert state.name == "Beer"
async def test_discovery_update_light(hass, mqtt_mock, caplog):
async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog):
"""Test update of discovered light."""
data1 = (
'{ "name": "Beer",'
' "state_topic": "test_topic",'
' "command_topic": "test_topic",'
' "state_value_template": "{{value_json.power1}}" }'
data1 = json.dumps(
{
"name": "Beer",
"state_topic": "test_light_rgb/state1",
"command_topic": "test_light_rgb/set",
"brightness_command_topic": "test_light_rgb/state1",
"rgb_command_topic": "test_light_rgb/rgb/set",
"color_temp_command_topic": "test_light_rgb/state1",
"effect_command_topic": "test_light_rgb/effect/set",
"hs_command_topic": "test_light_rgb/hs/set",
"white_value_command_topic": "test_light_rgb/white_value/set",
"xy_command_topic": "test_light_rgb/xy/set",
"brightness_state_topic": "test_light_rgb/state1",
"color_temp_state_topic": "test_light_rgb/state1",
"effect_state_topic": "test_light_rgb/state1",
"hs_state_topic": "test_light_rgb/state1",
"rgb_state_topic": "test_light_rgb/state1",
"white_value_state_topic": "test_light_rgb/state1",
"xy_state_topic": "test_light_rgb/state1",
"state_value_template": "{{ value_json.state1.state }}",
"brightness_value_template": "{{ value_json.state1.brightness }}",
"color_temp_value_template": "{{ value_json.state1.ct }}",
"effect_value_template": "{{ value_json.state1.fx }}",
"hs_value_template": "{{ value_json.state1.hs }}",
"rgb_value_template": "{{ value_json.state1.rgb }}",
"white_value_template": "{{ value_json.state1.white }}",
"xy_value_template": "{{ value_json.state1.xy }}",
}
)
data2 = (
'{ "name": "Milk",'
' "state_topic": "test_topic",'
' "command_topic": "test_topic",'
' "state_value_template": "{{value_json.power2}}" }'
data2 = json.dumps(
{
"name": "Milk",
"state_topic": "test_light_rgb/state2",
"command_topic": "test_light_rgb/set",
"brightness_command_topic": "test_light_rgb/state2",
"rgb_command_topic": "test_light_rgb/rgb/set",
"color_temp_command_topic": "test_light_rgb/state2",
"effect_command_topic": "test_light_rgb/effect/set",
"hs_command_topic": "test_light_rgb/hs/set",
"white_value_command_topic": "test_light_rgb/white_value/set",
"xy_command_topic": "test_light_rgb/xy/set",
"brightness_state_topic": "test_light_rgb/state2",
"color_temp_state_topic": "test_light_rgb/state2",
"effect_state_topic": "test_light_rgb/state2",
"hs_state_topic": "test_light_rgb/state2",
"rgb_state_topic": "test_light_rgb/state2",
"white_value_state_topic": "test_light_rgb/state2",
"xy_state_topic": "test_light_rgb/state2",
"state_value_template": "{{ value_json.state2.state }}",
"brightness_value_template": "{{ value_json.state2.brightness }}",
"color_temp_value_template": "{{ value_json.state2.ct }}",
"effect_value_template": "{{ value_json.state2.fx }}",
"hs_value_template": "{{ value_json.state2.hs }}",
"rgb_value_template": "{{ value_json.state2.rgb }}",
"white_value_template": "{{ value_json.state2.white }}",
"xy_value_template": "{{ value_json.state2.xy }}",
}
)
state_data1 = [
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
)
],
"on",
[("brightness", 100), ("color_temp", 123), ("effect", "cycle")],
),
(
[("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')],
"off",
None,
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "hs":"1,2"}}',
)
],
"on",
[("hs_color", (1, 2))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"rgb":"255,127,63"}}',
)
],
"on",
[("rgb_color", (255, 127, 63))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"white":50, "xy":"0.3, 0.4"}}',
)
],
"on",
[("white_value", 50), ("xy_color", (0.3, 0.401))],
),
]
state_data2 = [
(
[
(
"test_light_rgb/state2",
'{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}',
)
],
"on",
[("brightness", 50), ("color_temp", 200), ("effect", "loop")],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
),
(
"test_light_rgb/state1",
'{"state2":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
),
(
"test_light_rgb/state2",
'{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
),
],
"on",
[("brightness", 50), ("color_temp", 200), ("effect", "loop")],
),
(
[("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')],
"on",
None,
),
(
[("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')],
"on",
None,
),
(
[("test_light_rgb/state2", '{"state1":{"state":"OFF"}}')],
"on",
None,
),
(
[("test_light_rgb/state2", '{"state2":{"state":"OFF"}}')],
"off",
None,
),
(
[
(
"test_light_rgb/state2",
'{"state2":{"state":"ON", "hs":"1.2,2.2"}}',
)
],
"on",
[("hs_color", (1.2, 2.2))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "hs":"1,2"}}',
),
(
"test_light_rgb/state1",
'{"state2":{"state":"ON", "hs":"1,2"}}',
),
(
"test_light_rgb/state2",
'{"state1":{"state":"ON", "hs":"1,2"}}',
),
],
"on",
[("hs_color", (1.2, 2.2))],
),
(
[
(
"test_light_rgb/state2",
'{"state2":{"rgb":"63,127,255"}}',
)
],
"on",
[("rgb_color", (63, 127, 255))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"rgb":"255,127,63"}}',
),
(
"test_light_rgb/state1",
'{"state2":{"rgb":"255,127,63"}}',
),
(
"test_light_rgb/state2",
'{"state1":{"rgb":"255,127,63"}}',
),
],
"on",
[("rgb_color", (63, 127, 255))],
),
(
[
(
"test_light_rgb/state2",
'{"state2":{"white":75, "xy":"0.4, 0.3"}}',
)
],
"on",
[("white_value", 75), ("xy_color", (0.4, 0.3))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"white":50, "xy":"0.3, 0.4"}}',
),
(
"test_light_rgb/state1",
'{"state2":{"white":50, "xy":"0.3, 0.4"}}',
),
(
"test_light_rgb/state2",
'{"state1":{"white":50, "xy":"0.3, 0.4"}}',
),
],
"on",
[("white_value", 75), ("xy_color", (0.4, 0.3))],
),
]
await help_test_discovery_update(
hass,
mqtt_mock,
@@ -1487,10 +1717,221 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog):
light.DOMAIN,
data1,
data2,
state_data1=[("test_topic", '{"power1":"ON"}')],
state1="on",
state_data2=[("test_topic", '{"power2":"OFF"}')],
state2="off",
state_data1=state_data1,
state_data2=state_data2,
)
async def test_discovery_update_light_template(hass, mqtt_mock, caplog):
"""Test update of discovered light."""
data1 = json.dumps(
{
"name": "Beer",
"state_topic": "test_light_rgb/state1",
"command_topic": "test_light_rgb/set",
"brightness_command_topic": "test_light_rgb/state1",
"rgb_command_topic": "test_light_rgb/rgb/set",
"color_temp_command_topic": "test_light_rgb/state1",
"effect_command_topic": "test_light_rgb/effect/set",
"hs_command_topic": "test_light_rgb/hs/set",
"white_value_command_topic": "test_light_rgb/white_value/set",
"xy_command_topic": "test_light_rgb/xy/set",
"brightness_state_topic": "test_light_rgb/state1",
"color_temp_state_topic": "test_light_rgb/state1",
"effect_state_topic": "test_light_rgb/state1",
"hs_state_topic": "test_light_rgb/state1",
"rgb_state_topic": "test_light_rgb/state1",
"white_value_state_topic": "test_light_rgb/state1",
"xy_state_topic": "test_light_rgb/state1",
"state_value_template": "{{ value_json.state1.state }}",
"brightness_value_template": "{{ value_json.state1.brightness }}",
"color_temp_value_template": "{{ value_json.state1.ct }}",
"effect_value_template": "{{ value_json.state1.fx }}",
"hs_value_template": "{{ value_json.state1.hs }}",
"rgb_value_template": "{{ value_json.state1.rgb }}",
"white_value_template": "{{ value_json.state1.white }}",
"xy_value_template": "{{ value_json.state1.xy }}",
}
)
data2 = json.dumps(
{
"name": "Milk",
"state_topic": "test_light_rgb/state1",
"command_topic": "test_light_rgb/set",
"brightness_command_topic": "test_light_rgb/state1",
"rgb_command_topic": "test_light_rgb/rgb/set",
"color_temp_command_topic": "test_light_rgb/state1",
"effect_command_topic": "test_light_rgb/effect/set",
"hs_command_topic": "test_light_rgb/hs/set",
"white_value_command_topic": "test_light_rgb/white_value/set",
"xy_command_topic": "test_light_rgb/xy/set",
"brightness_state_topic": "test_light_rgb/state1",
"color_temp_state_topic": "test_light_rgb/state1",
"effect_state_topic": "test_light_rgb/state1",
"hs_state_topic": "test_light_rgb/state1",
"rgb_state_topic": "test_light_rgb/state1",
"white_value_state_topic": "test_light_rgb/state1",
"xy_state_topic": "test_light_rgb/state1",
"state_value_template": "{{ value_json.state2.state }}",
"brightness_value_template": "{{ value_json.state2.brightness }}",
"color_temp_value_template": "{{ value_json.state2.ct }}",
"effect_value_template": "{{ value_json.state2.fx }}",
"hs_value_template": "{{ value_json.state2.hs }}",
"rgb_value_template": "{{ value_json.state2.rgb }}",
"white_value_template": "{{ value_json.state2.white }}",
"xy_value_template": "{{ value_json.state2.xy }}",
}
)
state_data1 = [
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
)
],
"on",
[("brightness", 100), ("color_temp", 123), ("effect", "cycle")],
),
(
[("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')],
"off",
None,
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "hs":"1,2"}}',
)
],
"on",
[("hs_color", (1, 2))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"rgb":"255,127,63"}}',
)
],
"on",
[("rgb_color", (255, 127, 63))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"white":50, "xy":"0.3, 0.4"}}',
)
],
"on",
[("white_value", 50), ("xy_color", (0.3, 0.401))],
),
]
state_data2 = [
(
[
(
"test_light_rgb/state1",
'{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}',
)
],
"on",
[("brightness", 50), ("color_temp", 200), ("effect", "loop")],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}',
),
],
"on",
[("brightness", 50), ("color_temp", 200), ("effect", "loop")],
),
(
[("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')],
"on",
None,
),
(
[("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')],
"off",
None,
),
(
[
(
"test_light_rgb/state1",
'{"state2":{"state":"ON", "hs":"1.2,2.2"}}',
)
],
"on",
[("hs_color", (1.2, 2.2))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"state":"ON", "hs":"1,2"}}',
)
],
"on",
[("hs_color", (1.2, 2.2))],
),
(
[
(
"test_light_rgb/state1",
'{"state2":{"rgb":"63,127,255"}}',
)
],
"on",
[("rgb_color", (63, 127, 255))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"rgb":"255,127,63"}}',
)
],
"on",
[("rgb_color", (63, 127, 255))],
),
(
[
(
"test_light_rgb/state1",
'{"state2":{"white":75, "xy":"0.4, 0.3"}}',
)
],
"on",
[("white_value", 75), ("xy_color", (0.4, 0.3))],
),
(
[
(
"test_light_rgb/state1",
'{"state1":{"white":50, "xy":"0.3, 0.4"}}',
)
],
"on",
[("white_value", 75), ("xy_color", (0.4, 0.3))],
),
]
await help_test_discovery_update(
hass,
mqtt_mock,
caplog,
light.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
+64 -4
View File
@@ -1,4 +1,5 @@
"""The tests for the MQTT sensor platform."""
import copy
from datetime import datetime, timedelta
import json
@@ -430,12 +431,71 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog):
await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data)
async def test_discovery_update_sensor(hass, mqtt_mock, caplog):
async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog):
"""Test update of discovered sensor."""
data1 = '{ "name": "Beer", "state_topic": "test_topic" }'
data2 = '{ "name": "Milk", "state_topic": "test_topic" }'
config = {"name": "test", "state_topic": "test_topic"}
config1 = copy.deepcopy(config)
config2 = copy.deepcopy(config)
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "sensor/state1"
config2["state_topic"] = "sensor/state2"
config1["value_template"] = "{{ value_json.state | int }}"
config2["value_template"] = "{{ value_json.state | int * 2 }}"
state_data1 = [
([("sensor/state1", '{"state":100}')], "100", None),
]
state_data2 = [
([("sensor/state1", '{"state":1000}')], "100", None),
([("sensor/state1", '{"state":1000}')], "100", None),
([("sensor/state2", '{"state":100}')], "200", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2
hass,
mqtt_mock,
caplog,
sensor.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog):
"""Test update of discovered sensor."""
config = {"name": "test", "state_topic": "test_topic"}
config1 = copy.deepcopy(config)
config2 = copy.deepcopy(config)
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "sensor/state1"
config2["state_topic"] = "sensor/state1"
config1["value_template"] = "{{ value_json.state | int }}"
config2["value_template"] = "{{ value_json.state | int * 2 }}"
state_data1 = [
([("sensor/state1", '{"state":100}')], "100", None),
]
state_data2 = [
([("sensor/state1", '{"state":100}')], "200", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass,
mqtt_mock,
caplog,
sensor.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
+70 -12
View File
@@ -1,4 +1,7 @@
"""The tests for the MQTT switch platform."""
import copy
import json
import pytest
from homeassistant.components import switch
@@ -304,20 +307,75 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog):
await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data)
async def test_discovery_update_switch(hass, mqtt_mock, caplog):
async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog):
"""Test update of discovered switch."""
data1 = (
'{ "name": "Beer",'
' "state_topic": "test_topic",'
' "command_topic": "test_topic" }'
)
data2 = (
'{ "name": "Milk",'
' "state_topic": "test_topic",'
' "command_topic": "test_topic" }'
)
config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "switch/state1"
config2["state_topic"] = "switch/state2"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None),
]
state_data2 = [
([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None),
([("switch/state2", '{"state2":{"state":"ON"}}')], "on", None),
([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None),
([("switch/state1", '{"state2":{"state":"OFF"}}')], "on", None),
([("switch/state2", '{"state1":{"state":"OFF"}}')], "on", None),
([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2
hass,
mqtt_mock,
caplog,
switch.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
async def test_discovery_update_switch_template(hass, mqtt_mock, caplog):
"""Test update of discovered switch."""
config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN])
config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN])
config1["name"] = "Beer"
config2["name"] = "Milk"
config1["state_topic"] = "switch/state1"
config2["state_topic"] = "switch/state1"
config1["value_template"] = "{{ value_json.state1.state }}"
config2["value_template"] = "{{ value_json.state2.state }}"
state_data1 = [
([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None),
]
state_data2 = [
([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None),
([("switch/state1", '{"state2":{"state":"ON"}}')], "on", None),
([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None),
([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None),
]
data1 = json.dumps(config1)
data2 = json.dumps(config2)
await help_test_discovery_update(
hass,
mqtt_mock,
caplog,
switch.DOMAIN,
data1,
data2,
state_data1=state_data1,
state_data2=state_data2,
)
+1 -1
View File
@@ -276,4 +276,4 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client):
assert resp.status == 200
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 2
assert len(hass.states.async_entity_ids("weather")) == 1
+5
View File
@@ -409,6 +409,11 @@ class MockPlexLibrarySection:
if self.title == "Photos":
return "photo"
@property
def TYPE(self):
"""Return the library type."""
return self.type
@property
def key(self):
"""Mock the key identifier property."""
@@ -16,6 +16,9 @@ from homeassistant.components.media_player.const import (
ATTR_MEDIA_TITLE,
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MP_DOMAIN,
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
@@ -499,6 +502,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Media Library"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == "library"
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
@@ -523,10 +527,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Apps"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 11
assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP
assert msg["result"]["children"][0]["title"] == "Satellite TV"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP
@@ -565,10 +571,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client):
assert msg["result"]
assert msg["result"]["title"] == "Channels"
assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY
assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS
assert msg["result"]["can_expand"]
assert not msg["result"]["can_play"]
assert len(msg["result"]["children"]) == 2
assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL
assert msg["result"]["children"][0]["title"] == "WhatsOn"
assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL
+92 -1
View File
@@ -17,13 +17,14 @@ from homeassistant.const import (
)
from homeassistant.core import Context, callback, split_entity_id
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import template
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.loader import bind_hass
from homeassistant.setup import async_setup_component, setup_component
from tests.async_mock import Mock, patch
from tests.common import get_test_home_assistant
from tests.common import async_mock_service, get_test_home_assistant
from tests.components.logbook.test_init import MockLazyEventPartialState
ENTITY_ID = "script.test"
@@ -615,3 +616,93 @@ async def test_concurrent_script(hass, concurrently):
assert not script.is_on(hass, "script.script1")
assert not script.is_on(hass, "script.script2")
async def test_script_variables(hass, caplog):
"""Test defining scripts."""
assert await async_setup_component(
hass,
"script",
{
"script": {
"script1": {
"variables": {
"test_var": "from_config",
"templated_config_var": "{{ var_from_service | default('config-default') }}",
},
"sequence": [
{
"service": "test.script",
"data": {
"value": "{{ test_var }}",
"templated_config_var": "{{ templated_config_var }}",
},
},
],
},
"script2": {
"variables": {
"test_var": "from_config",
},
"sequence": [
{
"service": "test.script",
"data": {
"value": "{{ test_var }}",
},
},
],
},
"script3": {
"variables": {
"test_var": "{{ break + 1 }}",
},
"sequence": [
{
"service": "test.script",
"data": {
"value": "{{ test_var }}",
},
},
],
},
}
},
)
mock_calls = async_mock_service(hass, "test", "script")
await hass.services.async_call(
"script", "script1", {"var_from_service": "hello"}, blocking=True
)
assert len(mock_calls) == 1
assert mock_calls[0].data["value"] == "from_config"
assert mock_calls[0].data["templated_config_var"] == "hello"
await hass.services.async_call(
"script", "script1", {"test_var": "from_service"}, blocking=True
)
assert len(mock_calls) == 2
assert mock_calls[1].data["value"] == "from_service"
assert mock_calls[1].data["templated_config_var"] == "config-default"
# Call script with vars but no templates in it
await hass.services.async_call(
"script", "script2", {"test_var": "from_service"}, blocking=True
)
assert len(mock_calls) == 3
assert mock_calls[2].data["value"] == "from_service"
assert "Error rendering variables" not in caplog.text
with pytest.raises(template.TemplateError):
await hass.services.async_call("script", "script3", blocking=True)
assert "Error rendering variables" in caplog.text
assert len(mock_calls) == 3
await hass.services.async_call("script", "script3", {"break": 0}, blocking=True)
assert len(mock_calls) == 4
assert mock_calls[3].data["value"] == "1"
+228
View File
@@ -4,6 +4,8 @@ from unittest.mock import patch
from homeassistant.bootstrap import async_from_config_dict
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_ICON,
EVENT_COMPONENT_LOADED,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
@@ -758,3 +760,229 @@ async def test_sun_renders_once_per_sensor(hass):
"{{ state_attr('sun.sun', 'elevation') }}",
"{{ state_attr('sun.sun', 'next_rising') }}",
}
async def test_self_referencing_sensor_loop(hass, caplog):
"""Test a self referencing sensor does not loop forever."""
await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
assert int(state.state) == 1
await hass.async_block_till_done()
assert int(state.state) == 1
async def test_self_referencing_sensor_with_icon_loop(hass, caplog):
"""Test a self referencing sensor loops forever with a valid self referencing icon."""
await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
"icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}",
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
assert int(state.state) == 2
assert state.attributes[ATTR_ICON] == "mdi:greater"
await hass.async_block_till_done()
assert int(state.state) == 2
async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog):
"""Test a self referencing sensor loop forevers with a valid self referencing icon."""
await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
"icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}",
"entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}",
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
assert int(state.state) == 3
assert state.attributes[ATTR_ICON] == "mdi:less"
assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic"
await hass.async_block_till_done()
assert int(state.state) == 3
async def test_self_referencing_entity_picture_loop(hass, caplog):
"""Test a self referencing sensor does not loop forever with a looping self referencing entity picture."""
await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ 1 }}",
"entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}",
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog.text
state = hass.states.get("sensor.test")
assert int(state.state) == 1
assert state.attributes[ATTR_ENTITY_PICTURE] == "1"
await hass.async_block_till_done()
assert int(state.state) == 1
async def test_self_referencing_icon_with_no_loop(hass, caplog):
"""Test a self referencing icon that does not loop."""
hass.states.async_set("sensor.heartworm_high_80", 10)
hass.states.async_set("sensor.heartworm_low_57", 10)
hass.states.async_set("sensor.heartworm_avg_64", 10)
hass.states.async_set("sensor.heartworm_avg_57", 10)
value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %}
extreme
{% elif (states.sensor.heartworm_avg_64.state|int >= 30) %}
high
{% elif (states.sensor.heartworm_avg_64.state|int >= 14) %}
moderate
{% elif (states.sensor.heartworm_avg_64.state|int >= 5) %}
slight
{% elif (states.sensor.heartworm_avg_57.state|int >= 5) %}
marginal
{% elif (states.sensor.heartworm_avg_57.state|int < 5) %}
none
{% endif %}"""
icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %}
mdi:hazard-lights
{% elif is_state('sensor.heartworm_risk',"high") %}
mdi:triangle-outline
{% elif is_state('sensor.heartworm_risk',"moderate") %}
mdi:alert-circle-outline
{% elif is_state('sensor.heartworm_risk',"slight") %}
mdi:exclamation
{% elif is_state('sensor.heartworm_risk',"marginal") %}
mdi:heart
{% elif is_state('sensor.heartworm_risk',"none") %}
mdi:snowflake
{% endif %}"""
await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"heartworm_risk": {
"value_template": value_template_str,
"icon_template": icon_template_str,
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 5
hass.states.async_set("sensor.heartworm_high_80", 10)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" not in caplog.text
state = hass.states.get("sensor.heartworm_risk")
assert state.state == "extreme"
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
await hass.async_block_till_done()
assert state.state == "extreme"
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
assert "Template loop detected" not in caplog.text
@@ -420,14 +420,20 @@ async def test_render_template_renders_template(
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"result": "State is: on"}
assert event == {
"result": "State is: on",
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
}
hass.states.async_set("light.test", "off")
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"result": "State is: off"}
assert event == {
"result": "State is: off",
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
}
async def test_render_template_manual_entity_ids_no_longer_needed(
@@ -453,14 +459,20 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"result": "State is: on"}
assert event == {
"result": "State is: on",
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
}
hass.states.async_set("light.test", "off")
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"result": "State is: off"}
assert event == {
"result": "State is: off",
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
}
async def test_render_template_with_error(
@@ -480,7 +492,10 @@ async def test_render_template_with_error(
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"result": None}
assert event == {
"result": None,
"listeners": {"all": True, "domains": [], "entities": []},
}
assert "my_unknown_var" in caplog.text
assert "TemplateError" in caplog.text
+106 -12
View File
@@ -79,6 +79,24 @@ def get_homekit_info_mock(model, pairing_status):
return mock_homekit_info
def get_zeroconf_info_mock(macaddress):
"""Return info for get_service_info for an zeroconf device."""
def mock_zc_info(service_type, name):
return ServiceInfo(
service_type,
name,
addresses=[b"\n\x00\x00\x14"],
port=80,
weight=0,
priority=0,
server="name.local.",
properties={b"macaddress": macaddress.encode()},
)
return mock_zc_info
async def test_setup(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.object(
@@ -94,7 +112,11 @@ async def test_setup(hass, mock_zeroconf):
assert len(mock_service_browser.mock_calls) == 1
expected_flow_calls = 0
for matching_components in zc_gen.ZEROCONF.values():
expected_flow_calls += len(matching_components)
domains = set()
for component in matching_components:
if len(component) == 1:
domains.add(component["domain"])
expected_flow_calls += len(domains)
assert len(mock_config_flow.mock_calls) == expected_flow_calls
# Test instance is set.
@@ -209,10 +231,77 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog):
assert "Failed to get info for device name" in caplog.text
async def test_zeroconf_match(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
"_http._tcp.local.",
"shelly108._http._tcp.local.",
ServiceStateChange.Added,
)
with patch.dict(
zc_gen.ZEROCONF,
{"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser:
mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock(
"FFAADDCC11DD"
)
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "shelly"
async def test_zeroconf_no_match(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
"""Call service update handler."""
handlers[0](
zeroconf,
"_http._tcp.local.",
"somethingelse._http._tcp.local.",
ServiceStateChange.Added,
)
with patch.dict(
zc_gen.ZEROCONF,
{"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
) as mock_service_browser:
mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock(
"FFAADDCC11DD"
)
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 0
async def test_homekit_match_partial_space(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -233,7 +322,9 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf):
async def test_homekit_match_partial_dash(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -254,7 +345,9 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf):
async def test_homekit_match_full(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -267,11 +360,6 @@ async def test_homekit_match_full(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
homekit_mock = get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED)
info = homekit_mock("_hap._tcp.local.", "BSB002._hap._tcp.local.")
import pprint
pprint.pprint(["homekit", info])
assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "hue"
@@ -280,7 +368,9 @@ async def test_homekit_match_full(hass, mock_zeroconf):
async def test_homekit_already_paired(hass, mock_zeroconf):
"""Test that an already paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -302,7 +392,9 @@ async def test_homekit_already_paired(hass, mock_zeroconf):
async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
"""Test that missing paring data is not sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -323,7 +415,9 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf):
async def test_homekit_not_paired(hass, mock_zeroconf):
"""Test that an not paired device is sent to homekit_controller."""
with patch.dict(
zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True
zc_gen.ZEROCONF,
{zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]},
clear=True,
), patch.object(
hass.config_entries.flow, "async_init"
) as mock_config_flow, patch.object(
@@ -8,6 +8,7 @@ import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.config import async_process_ha_core_config
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.network import NoURLAvailableError
from tests.async_mock import patch
from tests.common import MockConfigEntry, mock_platform
@@ -128,6 +129,22 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl):
assert result["reason"] == "authorize_url_timeout"
async def test_abort_if_no_url_available(hass, flow_handler, local_impl):
"""Check no_url_available generating authorization url."""
flow_handler.async_register_implementation(hass, local_impl)
flow = flow_handler()
flow.hass = hass
with patch.object(
local_impl, "async_generate_authorize_url", side_effect=NoURLAvailableError
):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "no_url_available"
async def test_abort_if_oauth_error(
hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request
):
+82 -5
View File
@@ -682,20 +682,33 @@ async def test_track_template_result_complex(hass):
hass.states.async_set("light.one", "on")
hass.states.async_set("lock.one", "locked")
async_track_template_result(
info = async_track_template_result(
hass, [TrackTemplate(template_complex, None)], specific_run_callback
)
await hass.async_block_till_done()
assert info.listeners == {"all": True, "domains": set(), "entities": set()}
hass.states.async_set("sensor.domain", "light")
await hass.async_block_till_done()
assert len(specific_runs) == 1
assert specific_runs[0].strip() == "['light.one']"
assert info.listeners == {
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
}
hass.states.async_set("sensor.domain", "lock")
await hass.async_block_till_done()
assert len(specific_runs) == 2
assert specific_runs[1].strip() == "['lock.one']"
assert info.listeners == {
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
}
hass.states.async_set("sensor.domain", "all")
await hass.async_block_till_done()
@@ -703,11 +716,17 @@ async def test_track_template_result_complex(hass):
assert "light.one" in specific_runs[2]
assert "lock.one" in specific_runs[2]
assert "sensor.domain" in specific_runs[2]
assert info.listeners == {"all": True, "domains": set(), "entities": set()}
hass.states.async_set("sensor.domain", "light")
await hass.async_block_till_done()
assert len(specific_runs) == 4
assert specific_runs[3].strip() == "['light.one']"
assert info.listeners == {
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
}
hass.states.async_set("light.two", "on")
await hass.async_block_till_done()
@@ -715,6 +734,11 @@ async def test_track_template_result_complex(hass):
assert "light.one" in specific_runs[4]
assert "light.two" in specific_runs[4]
assert "sensor.domain" not in specific_runs[4]
assert info.listeners == {
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
}
hass.states.async_set("light.three", "on")
await hass.async_block_till_done()
@@ -723,26 +747,51 @@ async def test_track_template_result_complex(hass):
assert "light.two" in specific_runs[5]
assert "light.three" in specific_runs[5]
assert "sensor.domain" not in specific_runs[5]
assert info.listeners == {
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
}
hass.states.async_set("sensor.domain", "lock")
await hass.async_block_till_done()
assert len(specific_runs) == 7
assert specific_runs[6].strip() == "['lock.one']"
assert info.listeners == {
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
}
hass.states.async_set("sensor.domain", "single_binary_sensor")
await hass.async_block_till_done()
assert len(specific_runs) == 8
assert specific_runs[7].strip() == "unknown"
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"binary_sensor.single", "sensor.domain"},
}
hass.states.async_set("binary_sensor.single", "binary_sensor_on")
await hass.async_block_till_done()
assert len(specific_runs) == 9
assert specific_runs[8].strip() == "binary_sensor_on"
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"binary_sensor.single", "sensor.domain"},
}
hass.states.async_set("sensor.domain", "lock")
await hass.async_block_till_done()
assert len(specific_runs) == 10
assert specific_runs[9].strip() == "['lock.one']"
assert info.listeners == {
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
}
async def test_track_template_result_with_wildcard(hass):
@@ -766,7 +815,7 @@ async def test_track_template_result_with_wildcard(hass):
hass.states.async_set("cover.office_window", "closed")
hass.states.async_set("cover.office_skylight", "open")
async_track_template_result(
info = async_track_template_result(
hass, [TrackTemplate(template_complex, None)], specific_run_callback
)
await hass.async_block_till_done()
@@ -774,6 +823,7 @@ async def test_track_template_result_with_wildcard(hass):
hass.states.async_set("cover.office_window", "open")
await hass.async_block_till_done()
assert len(specific_runs) == 1
assert info.listeners == {"all": True, "domains": set(), "entities": set()}
assert "cover.office_drapes=closed" in specific_runs[0]
assert "cover.office_window=open" in specific_runs[0]
@@ -808,11 +858,22 @@ async def test_track_template_result_with_group(hass):
def specific_run_callback(event, updates):
specific_runs.append(updates.pop().result)
async_track_template_result(
info = async_track_template_result(
hass, [TrackTemplate(template_complex, None)], specific_run_callback
)
await hass.async_block_till_done()
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {
"group.power_sensors",
"sensor.power_1",
"sensor.power_2",
"sensor.power_3",
},
}
hass.states.async_set("sensor.power_1", 100.1)
await hass.async_block_till_done()
assert len(specific_runs) == 1
@@ -851,10 +912,11 @@ async def test_track_template_result_and_conditional(hass):
def specific_run_callback(event, updates):
specific_runs.append(updates.pop().result)
async_track_template_result(
info = async_track_template_result(
hass, [TrackTemplate(template, None)], specific_run_callback
)
await hass.async_block_till_done()
assert info.listeners == {"all": False, "domains": set(), "entities": {"light.a"}}
hass.states.async_set("light.b", "on")
await hass.async_block_till_done()
@@ -864,11 +926,21 @@ async def test_track_template_result_and_conditional(hass):
await hass.async_block_till_done()
assert len(specific_runs) == 1
assert specific_runs[0] == "on"
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"light.a", "light.b"},
}
hass.states.async_set("light.b", "off")
await hass.async_block_till_done()
assert len(specific_runs) == 2
assert specific_runs[1] == "off"
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"light.a", "light.b"},
}
hass.states.async_set("light.a", "off")
await hass.async_block_till_done()
@@ -924,7 +996,7 @@ async def test_track_template_result_iterator(hass):
def filter_callback(event, updates):
filter_runs.append(updates.pop().result)
async_track_template_result(
info = async_track_template_result(
hass,
[
TrackTemplate(
@@ -939,6 +1011,11 @@ async def test_track_template_result_iterator(hass):
filter_callback,
)
await hass.async_block_till_done()
assert info.listeners == {
"all": False,
"domains": {"sensor"},
"entities": {"sensor.test"},
}
hass.states.async_set("sensor.test", 6)
await hass.async_block_till_done()
+53
View File
@@ -15,6 +15,7 @@ from homeassistant.helpers.network import (
)
from tests.async_mock import Mock, patch
from tests.common import mock_component
async def test_get_url_internal(hass: HomeAssistant):
@@ -799,3 +800,55 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant):
assert _get_external_url(hass, allow_ip=False) == "https://example.com"
assert _get_external_url(hass, require_standard_port=True) == "https://example.com"
assert _get_external_url(hass, require_ssl=True) == "https://example.com"
async def test_get_current_request_url_with_known_host(
hass: HomeAssistant, current_request
):
"""Test getting current request URL with known hosts addresses."""
hass.config.api = Mock(
use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None
)
assert hass.config.internal_url is None
with pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True)
# Ensure we accept localhost
with patch(
"homeassistant.helpers.network._get_request_host", return_value="localhost"
):
assert get_url(hass, require_current_request=True) == "http://localhost:8123"
with pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True, require_ssl=True)
with pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True, require_standard_port=True)
# Ensure we accept local loopback ip (e.g., 127.0.0.1)
with patch(
"homeassistant.helpers.network._get_request_host", return_value="127.0.0.8"
):
assert get_url(hass, require_current_request=True) == "http://127.0.0.8:8123"
with pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True, allow_ip=False)
# Ensure hostname from Supervisor is accepted transparently
mock_component(hass, "hassio")
hass.components.hassio.is_hassio = Mock(return_value=True)
hass.components.hassio.get_host_info = Mock(
return_value={"hostname": "homeassistant"}
)
with patch(
"homeassistant.helpers.network._get_request_host",
return_value="homeassistant.local",
):
assert (
get_url(hass, require_current_request=True)
== "http://homeassistant.local:8123"
)
with patch(
"homeassistant.helpers.network._get_request_host", return_value="unknown.local"
), pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True)
+39
View File
@@ -1785,3 +1785,42 @@ async def test_started_action(hass, caplog):
await hass.async_block_till_done()
assert log_message in caplog.text
async def test_set_variable(hass, caplog):
"""Test setting variables in scripts."""
sequence = cv.SCRIPT_SCHEMA(
[
{"variables": {"variable": "value"}},
{"service": "test.script", "data": {"value": "{{ variable }}"}},
]
)
script_obj = script.Script(hass, sequence, "test script", "test_domain")
mock_calls = async_mock_service(hass, "test", "script")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert mock_calls[0].data["value"] == "value"
async def test_set_redefines_variable(hass, caplog):
"""Test setting variables based on their current value."""
sequence = cv.SCRIPT_SCHEMA(
[
{"variables": {"variable": "1"}},
{"service": "test.script", "data": {"value": "{{ variable }}"}},
{"variables": {"variable": "{{ variable | int + 1 }}"}},
{"service": "test.script", "data": {"value": "{{ variable }}"}},
]
)
script_obj = script.Script(hass, sequence, "test script", "test_domain")
mock_calls = async_mock_service(hass, "test", "script")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert mock_calls[0].data["value"] == "1"
assert mock_calls[1].data["value"] == "2"
+112
View File
@@ -0,0 +1,112 @@
"""Test script variables."""
import pytest
from homeassistant.helpers import config_validation as cv, template
async def test_static_vars():
"""Test static vars."""
orig = {"hello": "world"}
var = cv.SCRIPT_VARIABLES_SCHEMA(orig)
rendered = var.async_render(None, None)
assert rendered is not orig
assert rendered == orig
async def test_static_vars_run_args():
"""Test static vars."""
orig = {"hello": "world"}
orig_copy = dict(orig)
var = cv.SCRIPT_VARIABLES_SCHEMA(orig)
rendered = var.async_render(None, {"hello": "override", "run": "var"})
assert rendered == {"hello": "override", "run": "var"}
# Make sure we don't change original vars
assert orig == orig_copy
async def test_static_vars_no_default():
"""Test static vars."""
orig = {"hello": "world"}
var = cv.SCRIPT_VARIABLES_SCHEMA(orig)
rendered = var.async_render(None, None, render_as_defaults=False)
assert rendered is not orig
assert rendered == orig
async def test_static_vars_run_args_no_default():
"""Test static vars."""
orig = {"hello": "world"}
orig_copy = dict(orig)
var = cv.SCRIPT_VARIABLES_SCHEMA(orig)
rendered = var.async_render(
None, {"hello": "override", "run": "var"}, render_as_defaults=False
)
assert rendered == {"hello": "world", "run": "var"}
# Make sure we don't change original vars
assert orig == orig_copy
async def test_template_vars(hass):
"""Test template vars."""
var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"})
rendered = var.async_render(hass, None)
assert rendered == {"hello": "2"}
async def test_template_vars_run_args(hass):
"""Test template vars."""
var = cv.SCRIPT_VARIABLES_SCHEMA(
{
"something": "{{ run_var_ex + 1 }}",
"something_2": "{{ run_var_ex + 1 }}",
}
)
rendered = var.async_render(
hass,
{
"run_var_ex": 5,
"something_2": 1,
},
)
assert rendered == {
"run_var_ex": 5,
"something": "6",
"something_2": 1,
}
async def test_template_vars_no_default(hass):
"""Test template vars."""
var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"})
rendered = var.async_render(hass, None, render_as_defaults=False)
assert rendered == {"hello": "2"}
async def test_template_vars_run_args_no_default(hass):
"""Test template vars."""
var = cv.SCRIPT_VARIABLES_SCHEMA(
{
"something": "{{ run_var_ex + 1 }}",
"something_2": "{{ run_var_ex + 1 }}",
}
)
rendered = var.async_render(
hass,
{
"run_var_ex": 5,
"something_2": 1,
},
render_as_defaults=False,
)
assert rendered == {
"run_var_ex": 5,
"something": "6",
"something_2": "6",
}
async def test_template_vars_error(hass):
"""Test template vars."""
var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"})
with pytest.raises(template.TemplateError):
var.async_render(hass, None)
+43 -3
View File
@@ -218,6 +218,23 @@ def test_integration_properties(hass):
assert integration.zeroconf is None
assert integration.ssdp is None
integration = loader.Integration(
hass,
"custom_components.hue",
None,
{
"name": "Philips Hue",
"domain": "hue",
"dependencies": ["test-dep"],
"zeroconf": [{"type": "_hue._tcp.local.", "name": "hue*"}],
"requirements": ["test-req==1.0.0"],
},
)
assert integration.is_built_in is False
assert integration.homekit is None
assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}]
assert integration.ssdp is None
async def test_integrations_only_once(hass):
"""Test that we load integrations only once."""
@@ -253,6 +270,25 @@ def _get_test_integration(hass, name, config_flow):
)
def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow):
"""Return a generated test integration with a zeroconf matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"zeroconf": [{"type": f"_{name}._tcp.local.", "name": f"{name}*"}],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
},
)
async def test_get_custom_components(hass):
"""Verify that custom components are cached."""
test_1_integration = _get_test_integration(hass, "test_1", False)
@@ -289,7 +325,9 @@ async def test_get_config_flows(hass):
async def test_get_zeroconf(hass):
"""Verify that custom components with zeroconf are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration(hass, "test_2", True)
test_2_integration = _get_test_integration_with_zeroconf_matcher(
hass, "test_2", True
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
@@ -297,8 +335,10 @@ async def test_get_zeroconf(hass):
"test_2": test_2_integration,
}
zeroconf = await loader.async_get_zeroconf(hass)
assert zeroconf["_test_1._tcp.local."] == ["test_1"]
assert zeroconf["_test_2._tcp.local."] == ["test_2"]
assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}]
assert zeroconf["_test_2._tcp.local."] == [
{"domain": "test_2", "name": "test_2*"}
]
async def test_get_homekit(hass):