mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 10:23:46 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b19fe17e76 | |||
| 47326b2295 | |||
| 951c373110 | |||
| b9b76b3519 | |||
| da6885af6c | |||
| bc2173747c | |||
| d0e6b3e268 | |||
| 172a02a605 | |||
| b6f868f629 | |||
| 5697f4b4e7 | |||
| 30f9e1b479 | |||
| fcbcebea9b | |||
| f81606cbf5 | |||
| 3240be0bb6 | |||
| 18be6cbadc | |||
| a002e9b12f | |||
| db64a9ebfa | |||
| 3fbde22cc4 | |||
| 758e60a58d | |||
| 5201410e39 | |||
| b1b7944012 | |||
| 8ef04268be | |||
| b107e87d38 | |||
| b0b9579778 | |||
| 7eade4029a | |||
| 3d4913348a | |||
| 1720b71d62 | |||
| 589086f0d0 | |||
| 6f8060dea7 | |||
| b8ef87d84c | |||
| 7370b0ffc6 | |||
| 209cf44e8e | |||
| b7dacabbe4 | |||
| 5098c35814 | |||
| 896df60f32 | |||
| b26ab2849b | |||
| 36f52a26f6 | |||
| f0295d562d | |||
| 081bd22e59 | |||
| 668c73010a | |||
| fe371f0438 | |||
| be28dc0bca | |||
| 4578baca3e |
+2
-1
@@ -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
|
||||
@@ -465,7 +466,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/vera/* @vangorra
|
||||
homeassistant/components/versasense/* @flamm3blemuff1n
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/version/* @fabaff @ludeeus
|
||||
homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey
|
||||
homeassistant/components/vicare/* @oischinger
|
||||
homeassistant/components/vilfo/* @ManneW
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.2.1",
|
||||
"androidtv[async]==0.0.49",
|
||||
"androidtv[async]==0.0.50",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
],
|
||||
"codeowners": ["@JeffLIrion"]
|
||||
|
||||
@@ -380,7 +380,7 @@ def adb_decorator(override_available=False):
|
||||
# An unforeseen exception occurred. Close the ADB connection so that
|
||||
# it doesn't happen over and over again, then raise the exception.
|
||||
await self.aftv.adb_close()
|
||||
self._available = False # pylint: disable=protected-access
|
||||
self._available = False
|
||||
raise
|
||||
|
||||
return _adb_exception_catcher
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -146,11 +146,12 @@ class FreeboxCallSensor(FreeboxSensor):
|
||||
def async_update_state(self) -> None:
|
||||
"""Update the Freebox call sensor."""
|
||||
self._call_list_for_type = []
|
||||
for call in self._router.call_list:
|
||||
if not call["new"]:
|
||||
continue
|
||||
if call["type"] == self._sensor_type:
|
||||
self._call_list_for_type.append(call)
|
||||
if self._router.call_list:
|
||||
for call in self._router.call_list:
|
||||
if not call["new"]:
|
||||
continue
|
||||
if call["type"] == self._sensor_type:
|
||||
self._call_list_for_type.append(call)
|
||||
|
||||
self._state = len(self._call_list_for_type)
|
||||
|
||||
|
||||
@@ -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==20200912.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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -180,6 +205,7 @@ def special_library_payload(parent_payload, special_type):
|
||||
media_content_type=parent_payload.media_content_type,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=parent_payload.children_media_class,
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +220,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 +256,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,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(
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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*"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -148,7 +148,8 @@ class HlsStreamOutput(StreamOutput):
|
||||
def container_options(self) -> Callable[[int], dict]:
|
||||
"""Return Callable which takes a sequence number and returns container options."""
|
||||
return lambda sequence: {
|
||||
"movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont",
|
||||
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
|
||||
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont",
|
||||
"avoid_negative_ts": "make_non_negative",
|
||||
"fragment_index": str(sequence),
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "version",
|
||||
"name": "Version",
|
||||
"documentation": "https://www.home-assistant.io/integrations/version",
|
||||
"requirements": ["pyhaversion==3.3.0"],
|
||||
"codeowners": ["@fabaff"],
|
||||
"requirements": ["pyhaversion==3.4.0"],
|
||||
"codeowners": ["@fabaff", "@ludeeus"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ ALL_IMAGES = [
|
||||
"raspberrypi4-64",
|
||||
"tinker",
|
||||
"odroid-c2",
|
||||
"odroid-n2",
|
||||
"odroid-xu",
|
||||
]
|
||||
ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"]
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 115
|
||||
PATCH_VERSION = "0b4"
|
||||
PATCH_VERSION = "0b8"
|
||||
__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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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==20200912.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
|
||||
|
||||
|
||||
@@ -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})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -248,7 +248,7 @@ ambiclimate==0.2.1
|
||||
amcrest==1.7.0
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.49
|
||||
androidtv[async]==0.0.50
|
||||
|
||||
# homeassistant.components.anel_pwrctrl
|
||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||
@@ -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==20200912.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -1377,7 +1377,7 @@ pygtfs==0.1.5
|
||||
pygti==0.6.0
|
||||
|
||||
# homeassistant.components.version
|
||||
pyhaversion==3.3.0
|
||||
pyhaversion==3.4.0
|
||||
|
||||
# homeassistant.components.heos
|
||||
pyheos==0.6.0
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -149,7 +149,7 @@ airly==0.0.2
|
||||
ambiclimate==0.2.1
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.49
|
||||
androidtv[async]==0.0.50
|
||||
|
||||
# homeassistant.components.apns
|
||||
apns2==0.3.0
|
||||
@@ -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==20200912.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -662,7 +662,7 @@ pygatt[GATTTOOL]==4.0.5
|
||||
pygti==0.6.0
|
||||
|
||||
# homeassistant.components.version
|
||||
pyhaversion==3.3.0
|
||||
pyhaversion==3.4.0
|
||||
|
||||
# homeassistant.components.heos
|
||||
pyheos==0.6.0
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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))])
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user