mirror of
https://github.com/home-assistant/core.git
synced 2026-05-04 20:04:35 +02:00
Compare commits
44 Commits
2022.5.0b4
...
2022.5.0b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac5dd32a8 | |||
| 06f939b0ac | |||
| d213cc3c8e | |||
| 6cac24887c | |||
| 71506bd02b | |||
| 2b87e03fac | |||
| 4dfcc9dcf9 | |||
| fed4d0a38d | |||
| c37fec67a1 | |||
| 35bc812397 | |||
| d97bce2d9d | |||
| c07e36283b | |||
| b558425333 | |||
| 6bb3957730 | |||
| b8e0066ec2 | |||
| 72eb963c7a | |||
| 494902e185 | |||
| 0ec29711e1 | |||
| 78439eebb9 | |||
| d4780ac43c | |||
| 5c4861011b | |||
| c791c52d28 | |||
| 0d4947a2d3 | |||
| 1f912e9c98 | |||
| db53b3cbe0 | |||
| e7bcf839ac | |||
| 79cc216327 | |||
| 01b096bb09 | |||
| 174717dd85 | |||
| 4751356638 | |||
| 47d19b3967 | |||
| c1bbcfd275 | |||
| adc2f3d169 | |||
| 6a110e5a77 | |||
| 3ce531e2f1 | |||
| ce73b517b8 | |||
| 4f784c42ab | |||
| c0b6d6a44e | |||
| 4346d8cc2f | |||
| 4c7c7b72b7 | |||
| 26b6952c06 | |||
| 5d37cfc61e | |||
| 51aa070e19 | |||
| 205a8fc752 |
@@ -18,7 +18,7 @@ def build_app_list(app_list):
|
||||
|
||||
return BrowseMedia(
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id=None,
|
||||
media_content_id="apps",
|
||||
media_content_type=MEDIA_TYPE_APPS,
|
||||
title="Apps",
|
||||
can_play=True,
|
||||
|
||||
@@ -13,11 +13,15 @@ from pyatv.const import (
|
||||
)
|
||||
from pyatv.helpers import is_streamable
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
@@ -73,7 +77,8 @@ SUPPORT_APPLE_TV = (
|
||||
|
||||
# Map features in pyatv to Home Assistant
|
||||
SUPPORT_FEATURE_MAPPING = {
|
||||
FeatureName.PlayUrl: MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
FeatureName.StreamFile: MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
FeatureName.Pause: MediaPlayerEntityFeature.PAUSE,
|
||||
FeatureName.Play: MediaPlayerEntityFeature.PLAY,
|
||||
@@ -276,12 +281,24 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
|
||||
# RAOP. Otherwise try to play it with regular AirPlay.
|
||||
if media_type == MEDIA_TYPE_APP:
|
||||
await self.atv.apps.launch_app(media_id)
|
||||
elif self._is_feature_available(FeatureName.StreamFile) and (
|
||||
await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC
|
||||
|
||||
is_media_source_id = media_source.is_media_source_id(media_id)
|
||||
|
||||
if (
|
||||
not is_media_source_id
|
||||
and self._is_feature_available(FeatureName.StreamFile)
|
||||
and (await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC)
|
||||
):
|
||||
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
||||
await self.atv.stream.stream_file(media_id)
|
||||
elif self._is_feature_available(FeatureName.PlayUrl):
|
||||
|
||||
if self._is_feature_available(FeatureName.PlayUrl):
|
||||
if is_media_source_id:
|
||||
play_item = await media_source.async_resolve_media(self.hass, media_id)
|
||||
media_id = play_item.url
|
||||
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
||||
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
||||
await self.atv.stream.play_url(media_id)
|
||||
else:
|
||||
@@ -380,7 +397,34 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
|
||||
media_content_id=None,
|
||||
) -> BrowseMedia:
|
||||
"""Implement the websocket media browsing helper."""
|
||||
return build_app_list(self._app_list)
|
||||
# If we can't stream URLs, we can't browse media.
|
||||
# In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp
|
||||
if not self._is_feature_available(FeatureName.PlayUrl):
|
||||
return build_app_list(self._app_list)
|
||||
|
||||
if self._app_list:
|
||||
kwargs = {}
|
||||
else:
|
||||
# If it has no apps, assume it has no display
|
||||
kwargs = {
|
||||
"content_filter": lambda item: item.media_content_type.startswith(
|
||||
"audio/"
|
||||
),
|
||||
}
|
||||
|
||||
cur_item = await media_source.async_browse_media(
|
||||
self.hass, media_content_id, **kwargs
|
||||
)
|
||||
|
||||
# If media content id is not None, we're browsing into a media source
|
||||
if media_content_id is not None:
|
||||
return cur_item
|
||||
|
||||
# Add app item if we have one
|
||||
if self._app_list and cur_item.children:
|
||||
cur_item.children.insert(0, build_app_list(self._app_list))
|
||||
|
||||
return cur_item
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
CONF_CONDITION,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT_DATA,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_PLATFORM,
|
||||
@@ -360,9 +361,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
referenced |= condition.async_extract_devices(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
device = _trigger_extract_device(conf)
|
||||
if device is not None:
|
||||
referenced.add(device)
|
||||
referenced |= set(_trigger_extract_device(conf))
|
||||
|
||||
self._referenced_devices = referenced
|
||||
return referenced
|
||||
@@ -764,12 +763,22 @@ async def _async_process_if(hass, name, config, p_config):
|
||||
|
||||
|
||||
@callback
|
||||
def _trigger_extract_device(trigger_conf: dict) -> str | None:
|
||||
def _trigger_extract_device(trigger_conf: dict) -> list[str]:
|
||||
"""Extract devices from a trigger config."""
|
||||
if trigger_conf[CONF_PLATFORM] != "device":
|
||||
return None
|
||||
if trigger_conf[CONF_PLATFORM] == "device":
|
||||
return [trigger_conf[CONF_DEVICE_ID]]
|
||||
|
||||
return trigger_conf[CONF_DEVICE_ID]
|
||||
if (
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
|
||||
return trigger_conf[CONF_DEVICE_ID]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
@@ -778,6 +787,9 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
|
||||
return trigger_conf[CONF_ENTITY_ID]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "calendar":
|
||||
return [trigger_conf[CONF_ENTITY_ID]]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "zone":
|
||||
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]]
|
||||
|
||||
@@ -787,4 +799,11 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
if trigger_conf[CONF_PLATFORM] == "sun":
|
||||
return ["sun.sun"]
|
||||
|
||||
if (
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
|
||||
|
||||
return []
|
||||
|
||||
@@ -23,7 +23,7 @@ async def async_setup(hass):
|
||||
if action != ACTION_DELETE:
|
||||
return
|
||||
|
||||
ent_reg = await entity_registry.async_get_registry(hass)
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ async def async_setup(hass):
|
||||
if action != ACTION_DELETE:
|
||||
return
|
||||
|
||||
ent_reg = await entity_registry.async_get_registry(hass)
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
entity_id = ent_reg.async_get_entity_id(DOMAIN, HA_DOMAIN, config_key)
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ from homeassistant.components.script.config import (
|
||||
)
|
||||
from homeassistant.config import SCRIPT_CONFIG_PATH
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
|
||||
from . import EditKeyBasedConfigView
|
||||
from . import ACTION_DELETE, EditKeyBasedConfigView
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
@@ -18,6 +18,18 @@ async def async_setup(hass):
|
||||
"""post_write_hook for Config View that reloads scripts."""
|
||||
await hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
if action != ACTION_DELETE:
|
||||
return
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key)
|
||||
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
ent_reg.async_remove(entity_id)
|
||||
|
||||
hass.http.register_view(
|
||||
EditScriptConfigView(
|
||||
DOMAIN,
|
||||
|
||||
@@ -215,12 +215,6 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
|
||||
"""Handle a discovered deCONZ bridge."""
|
||||
if (
|
||||
discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL)
|
||||
!= DECONZ_MANUFACTURERURL
|
||||
):
|
||||
return self.async_abort(reason="not_deconz_bridge")
|
||||
|
||||
LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info))
|
||||
|
||||
self.bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
|
||||
|
||||
@@ -403,7 +403,7 @@ AQARA_OPPLE_4_BUTTONS = {
|
||||
AQARA_OPPLE_6_BUTTONS_MODEL = "lumi.remote.b686opcn01"
|
||||
AQARA_OPPLE_6_BUTTONS = {
|
||||
**AQARA_OPPLE_4_BUTTONS,
|
||||
(CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 5001},
|
||||
(CONF_LONG_PRESS, CONF_LEFT): {CONF_EVENT: 5001},
|
||||
(CONF_SHORT_RELEASE, CONF_LEFT): {CONF_EVENT: 5002},
|
||||
(CONF_LONG_RELEASE, CONF_LEFT): {CONF_EVENT: 5003},
|
||||
(CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 5004},
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"requirements": ["pydeconz==91"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
"manufacturerURL": "http://www.dresden-elektronik.de"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@Kane610"],
|
||||
|
||||
@@ -112,7 +112,7 @@ ENTITY_DESCRIPTIONS = {
|
||||
DeconzSensorDescription(
|
||||
key="consumption",
|
||||
value_fn=lambda device: device.scaled_consumption
|
||||
if isinstance(device, Consumption)
|
||||
if isinstance(device, Consumption) and isinstance(device.consumption, int)
|
||||
else None,
|
||||
update_key="consumption",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
@@ -144,7 +144,7 @@ ENTITY_DESCRIPTIONS = {
|
||||
DeconzSensorDescription(
|
||||
key="humidity",
|
||||
value_fn=lambda device: device.scaled_humidity
|
||||
if isinstance(device, Humidity)
|
||||
if isinstance(device, Humidity) and isinstance(device.humidity, int)
|
||||
else None,
|
||||
update_key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
@@ -156,7 +156,7 @@ ENTITY_DESCRIPTIONS = {
|
||||
DeconzSensorDescription(
|
||||
key="light_level",
|
||||
value_fn=lambda device: device.scaled_light_level
|
||||
if isinstance(device, LightLevel)
|
||||
if isinstance(device, LightLevel) and isinstance(device.light_level, int)
|
||||
else None,
|
||||
update_key="lightlevel",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
@@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS = {
|
||||
DeconzSensorDescription(
|
||||
key="temperature",
|
||||
value_fn=lambda device: device.scaled_temperature
|
||||
if isinstance(device, Temperature)
|
||||
if isinstance(device, Temperature) and isinstance(device.temperature, int)
|
||||
else None,
|
||||
update_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"no_hardware_available": "No radio hardware connected to deCONZ",
|
||||
"not_deconz_bridge": "Not a deCONZ bridge",
|
||||
"updated_instance": "Updated deCONZ instance with new host address"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220429.0"],
|
||||
"requirements": ["home-assistant-frontend==20220502.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -130,10 +130,9 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]:
|
||||
fmt = None
|
||||
if not (url := info.get(CONF_STILL_IMAGE_URL)):
|
||||
return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg")
|
||||
if not isinstance(url, template_helper.Template) and url:
|
||||
url = cv.template(url)
|
||||
url.hass = hass
|
||||
try:
|
||||
if not isinstance(url, template_helper.Template):
|
||||
url = template_helper.Template(url, hass)
|
||||
url = url.async_render(parse_result=False)
|
||||
except TemplateError as err:
|
||||
_LOGGER.warning("Problem rendering template %s: %s", url, err)
|
||||
@@ -168,11 +167,20 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]:
|
||||
return {}, f"image/{fmt}"
|
||||
|
||||
|
||||
def slug_url(url) -> str | None:
|
||||
def slug(hass, template) -> str | None:
|
||||
"""Convert a camera url into a string suitable for a camera name."""
|
||||
if not url:
|
||||
if not template:
|
||||
return None
|
||||
return slugify(yarl.URL(url).host)
|
||||
if not isinstance(template, template_helper.Template):
|
||||
template = template_helper.Template(template, hass)
|
||||
try:
|
||||
url = template.async_render(parse_result=False)
|
||||
return slugify(yarl.URL(url).host)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Syntax error in '%s': %s", template.template, err)
|
||||
except (ValueError, TypeError) as err:
|
||||
_LOGGER.error("Syntax error in '%s': %s", url, err)
|
||||
return None
|
||||
|
||||
|
||||
async def async_test_stream(hass, info) -> dict[str, str]:
|
||||
@@ -252,6 +260,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> FlowResult:
|
||||
"""Handle the start of the config flow."""
|
||||
errors = {}
|
||||
hass = self.hass
|
||||
if user_input:
|
||||
# Secondary validation because serialised vol can't seem to handle this complexity:
|
||||
if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get(
|
||||
@@ -263,8 +272,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = errors | await async_test_stream(self.hass, user_input)
|
||||
still_url = user_input.get(CONF_STILL_IMAGE_URL)
|
||||
stream_url = user_input.get(CONF_STREAM_SOURCE)
|
||||
name = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
|
||||
|
||||
name = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME
|
||||
if not errors:
|
||||
user_input[CONF_CONTENT_TYPE] = still_format
|
||||
user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
|
||||
@@ -295,7 +303,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
still_url = import_config.get(CONF_STILL_IMAGE_URL)
|
||||
stream_url = import_config.get(CONF_STREAM_SOURCE)
|
||||
name = import_config.get(
|
||||
CONF_NAME, slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
|
||||
CONF_NAME,
|
||||
slug(self.hass, still_url) or slug(self.hass, stream_url) or DEFAULT_NAME,
|
||||
)
|
||||
if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config:
|
||||
import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
|
||||
@@ -318,6 +327,7 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
||||
) -> FlowResult:
|
||||
"""Manage Generic IP Camera options."""
|
||||
errors: dict[str, str] = {}
|
||||
hass = self.hass
|
||||
|
||||
if user_input is not None:
|
||||
errors, still_format = await async_test_still(
|
||||
@@ -327,7 +337,7 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
||||
still_url = user_input.get(CONF_STILL_IMAGE_URL)
|
||||
stream_url = user_input.get(CONF_STREAM_SOURCE)
|
||||
if not errors:
|
||||
title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
|
||||
title = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME
|
||||
if still_url is None:
|
||||
# If user didn't specify a still image URL,
|
||||
# The automatically generated still image that stream generates
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["auth"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==0.6.3", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==0.7.1", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -4,6 +4,7 @@ from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from pyinsteon import async_close, async_connect, devices
|
||||
from pyinsteon.constants import ReadWriteMode
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP
|
||||
@@ -48,7 +49,8 @@ async def async_get_device_config(hass, config_entry):
|
||||
with suppress(AttributeError):
|
||||
await devices[address].async_status()
|
||||
|
||||
await devices.async_load(id_devices=1)
|
||||
load_aldb = devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN
|
||||
await devices.async_load(id_devices=1, load_modem_aldb=load_aldb)
|
||||
for addr in devices:
|
||||
device = devices[addr]
|
||||
flags = True
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/insteon",
|
||||
"dependencies": ["http", "websocket_api"],
|
||||
"requirements": [
|
||||
"pyinsteon==1.1.0b3",
|
||||
"pyinsteon==1.1.0",
|
||||
"insteon-frontend-home-assistant==0.1.0"
|
||||
],
|
||||
"codeowners": ["@teharris1"],
|
||||
|
||||
@@ -196,10 +196,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
# or device_class.
|
||||
update_state = False
|
||||
|
||||
if self._unit_of_measurement is None:
|
||||
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit is not None:
|
||||
self._unit_of_measurement = self._unit_template.format(unit)
|
||||
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit is not None:
|
||||
new_unit_of_measurement = self._unit_template.format(unit)
|
||||
if self._unit_of_measurement != new_unit_of_measurement:
|
||||
self._unit_of_measurement = new_unit_of_measurement
|
||||
update_state = True
|
||||
|
||||
if (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "KNX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||
"requirements": ["xknx==0.20.4"],
|
||||
"requirements": ["xknx==0.21.1"],
|
||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -24,6 +24,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add kostal plenticore Select widget."""
|
||||
plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
available_settings_data = await plenticore.client.get_settings()
|
||||
select_data_update_coordinator = SelectDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@@ -32,23 +34,34 @@ async def async_setup_entry(
|
||||
plenticore,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
PlenticoreDataSelect(
|
||||
select_data_update_coordinator,
|
||||
entry_id=entry.entry_id,
|
||||
platform_name=entry.title,
|
||||
device_class="kostal_plenticore__battery",
|
||||
module_id=select.module_id,
|
||||
data_id=select.data_id,
|
||||
name=select.name,
|
||||
current_option="None",
|
||||
options=select.options,
|
||||
is_on=select.is_on,
|
||||
device_info=plenticore.device_info,
|
||||
unique_id=f"{entry.entry_id}_{select.module_id}",
|
||||
entities = []
|
||||
for select in SELECT_SETTINGS_DATA:
|
||||
if select.module_id not in available_settings_data:
|
||||
continue
|
||||
needed_data_ids = {data_id for data_id in select.options if data_id != "None"}
|
||||
available_data_ids = {
|
||||
setting.id for setting in available_settings_data[select.module_id]
|
||||
}
|
||||
if not needed_data_ids <= available_data_ids:
|
||||
continue
|
||||
entities.append(
|
||||
PlenticoreDataSelect(
|
||||
select_data_update_coordinator,
|
||||
entry_id=entry.entry_id,
|
||||
platform_name=entry.title,
|
||||
device_class="kostal_plenticore__battery",
|
||||
module_id=select.module_id,
|
||||
data_id=select.data_id,
|
||||
name=select.name,
|
||||
current_option="None",
|
||||
options=select.options,
|
||||
is_on=select.is_on,
|
||||
device_info=plenticore.device_info,
|
||||
unique_id=f"{entry.entry_id}_{select.module_id}",
|
||||
)
|
||||
)
|
||||
for select in SELECT_SETTINGS_DATA
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC):
|
||||
|
||||
@@ -294,6 +294,8 @@ async def async_unload_entry(
|
||||
class LutronCasetaDevice(Entity):
|
||||
"""Common base class for all Lutron Caseta devices."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, device, bridge, bridge_device):
|
||||
"""Set up the base class.
|
||||
|
||||
@@ -304,6 +306,18 @@ class LutronCasetaDevice(Entity):
|
||||
self._device = device
|
||||
self._smartbridge = bridge
|
||||
self._bridge_device = bridge_device
|
||||
info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.serial)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=f"{device['model']} ({device['type']})",
|
||||
name=self.name,
|
||||
via_device=(DOMAIN, self._bridge_device["serial"]),
|
||||
configuration_url="https://device-login.lutron.com",
|
||||
)
|
||||
area, _ = _area_and_name_from_name(device["name"])
|
||||
if area != UNASSIGNED_AREA:
|
||||
info[ATTR_SUGGESTED_AREA] = area
|
||||
self._attr_device_info = info
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -329,28 +343,7 @@ class LutronCasetaDevice(Entity):
|
||||
"""Return the unique ID of the device (serial)."""
|
||||
return str(self.serial)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
device = self._device
|
||||
info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.serial)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=f"{device['model']} ({device['type']})",
|
||||
name=self.name,
|
||||
via_device=(DOMAIN, self._bridge_device["serial"]),
|
||||
configuration_url="https://device-login.lutron.com",
|
||||
)
|
||||
area, _ = _area_and_name_from_name(device["name"])
|
||||
if area != UNASSIGNED_AREA:
|
||||
info[ATTR_SUGGESTED_AREA] = area
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {"device_id": self.device_id, "zone_id": self._device["zone"]}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@@ -20,6 +20,9 @@ from homeassistant.helpers.network import (
|
||||
|
||||
from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY
|
||||
|
||||
# Paths that we don't need to sign
|
||||
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/",)
|
||||
|
||||
|
||||
@callback
|
||||
def async_process_play_media_url(
|
||||
@@ -46,6 +49,10 @@ def async_process_play_media_url(
|
||||
logging.getLogger(__name__).debug(
|
||||
"Not signing path for content with query param"
|
||||
)
|
||||
elif parsed.path.startswith(PATHS_WITHOUT_AUTH):
|
||||
# We don't sign this path if it doesn't need auth. Although signing itself can't hurt,
|
||||
# some devices are unable to handle long URLs and the auth signature might push it over.
|
||||
pass
|
||||
else:
|
||||
signed_path = async_sign_path(
|
||||
hass,
|
||||
|
||||
@@ -59,7 +59,9 @@ def _get_vehicle_diagnostics(vehicle: RenaultVehicleProxy) -> dict[str, Any]:
|
||||
return {
|
||||
"details": async_redact_data(vehicle.details.raw_data, TO_REDACT),
|
||||
"data": {
|
||||
key: async_redact_data(coordinator.data.raw_data, TO_REDACT)
|
||||
key: async_redact_data(
|
||||
coordinator.data.raw_data if coordinator.data else None, TO_REDACT
|
||||
)
|
||||
for key, coordinator in vehicle.coordinators.items()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -209,14 +209,11 @@ class SAJsensor(SensorEntity):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class the sensor belongs to."""
|
||||
if self.unit_of_measurement == POWER_WATT:
|
||||
if self.native_unit_of_measurement == POWER_WATT:
|
||||
return SensorDeviceClass.POWER
|
||||
if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR:
|
||||
if self.native_unit_of_measurement == ENERGY_KILO_WATT_HOUR:
|
||||
return SensorDeviceClass.ENERGY
|
||||
if (
|
||||
self.unit_of_measurement == TEMP_CELSIUS
|
||||
or self._sensor.unit == TEMP_FAHRENHEIT
|
||||
):
|
||||
if self.native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
|
||||
@property
|
||||
|
||||
@@ -45,7 +45,7 @@ HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()}
|
||||
AC_STATE_TO_DATA = {
|
||||
"targetTemperature": "target_temp",
|
||||
"fanLevel": "fan_mode",
|
||||
"on": "on",
|
||||
"on": "device_on",
|
||||
"mode": "hvac_mode",
|
||||
"swing": "swing_mode",
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sensibo",
|
||||
"name": "Sensibo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
||||
"requirements": ["pysensibo==1.0.12"],
|
||||
"requirements": ["pysensibo==1.0.14"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==2022.04.1"],
|
||||
"requirements": ["simplisafe-python==2022.05.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"dhcp": [
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"progress": {
|
||||
"email_2fa": "Input the two-factor authentication code\nsent to you via email."
|
||||
"email_2fa": "Check your email for a verification link from Simplisafe."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -3,23 +3,16 @@
|
||||
"abort": {
|
||||
"already_configured": "This SimpliSafe account is already in use.",
|
||||
"email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.",
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"wrong_account": "The user credentials provided do not match this SimpliSafe account."
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"error": {
|
||||
"2fa_timed_out": "Timed out while waiting for two-factor authentication",
|
||||
"identifier_exists": "Account already registered",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"still_awaiting_mfa": "Still awaiting MFA email click",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"progress": {
|
||||
"email_2fa": "Input the two-factor authentication code\nsent to you via email."
|
||||
"email_2fa": "Check your email for a verification link from Simplisafe."
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"title": "SimpliSafe Multi-Factor Authentication"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Password"
|
||||
@@ -35,13 +28,10 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"auth_code": "Authorization Code",
|
||||
"code": "Code (used in Home Assistant UI)",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Input your username and password.",
|
||||
"title": "Fill in your information."
|
||||
"description": "Input your username and password."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -100,8 +100,8 @@ class SlimProtoPlayer(MediaPlayerEntity):
|
||||
name=self.player.name,
|
||||
hw_version=self.player.firmware,
|
||||
)
|
||||
# PiCore player has web interface
|
||||
if "-pCP" in self.player.firmware:
|
||||
# PiCore + SqueezeESP32 player has web interface
|
||||
if "-pCP" in self.player.firmware or self.player.device_model == "SqueezeESP32":
|
||||
self._attr_device_info[
|
||||
"configuration_url"
|
||||
] = f"http://{self.player.device_address}"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smarttub",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@mdz"],
|
||||
"requirements": ["python-smarttub==0.0.31"],
|
||||
"requirements": ["python-smarttub==0.0.32"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["smarttub"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for interface with a Bose Soundtouch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -8,11 +9,15 @@ from libsoundtouch import soundtouch_device
|
||||
from libsoundtouch.utils import Source
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@@ -190,6 +195,7 @@ class SoundTouchDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, name, config):
|
||||
@@ -348,6 +354,16 @@ class SoundTouchDevice(MediaPlayerEntity):
|
||||
EVENT_HOMEASSISTANT_START, async_update_on_start
|
||||
)
|
||||
|
||||
async def async_play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
if media_source.is_media_source_id(media_id):
|
||||
play_item = await media_source.async_resolve_media(self.hass, media_id)
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.play_media, media_type, media_id, **kwargs)
|
||||
)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
_LOGGER.debug("Starting media with media_id: %s", media_id)
|
||||
@@ -450,6 +466,10 @@ class SoundTouchDevice(MediaPlayerEntity):
|
||||
|
||||
return attributes
|
||||
|
||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||
"""Implement the websocket media browsing helper."""
|
||||
return await media_source.async_browse_media(self.hass, media_content_id)
|
||||
|
||||
def get_zone_info(self):
|
||||
"""Return the current zone info."""
|
||||
zone_status = self._device.zone_status()
|
||||
|
||||
@@ -42,7 +42,7 @@ _QUERY_SCHEME = vol.Schema(
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_QUERY): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,14 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DEFAULT_NAME, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_ACCOUNT,
|
||||
CONF_ACCOUNTS,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
PLACEHOLDERS,
|
||||
)
|
||||
|
||||
|
||||
def validate_input(user_input: dict[str, str | int]) -> list[dict[str, str | int]]:
|
||||
@@ -52,14 +59,14 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if res[0] is not None:
|
||||
name = str(res[0]["personaname"])
|
||||
else:
|
||||
errors = {"base": "invalid_account"}
|
||||
errors["base"] = "invalid_account"
|
||||
except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex:
|
||||
errors = {"base": "cannot_connect"}
|
||||
errors["base"] = "cannot_connect"
|
||||
if "403" in str(ex):
|
||||
errors = {"base": "invalid_auth"}
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as ex: # pylint:disable=broad-except
|
||||
LOGGER.exception("Unknown exception: %s", ex)
|
||||
errors = {"base": "unknown"}
|
||||
errors["base"] = "unknown"
|
||||
if not errors:
|
||||
entry = await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||
if entry and self.source == config_entries.SOURCE_REAUTH:
|
||||
@@ -70,20 +77,12 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self.source == config_entries.SOURCE_IMPORT:
|
||||
accounts_data = {
|
||||
CONF_ACCOUNTS: {
|
||||
acc["steamid"]: {
|
||||
"name": acc["personaname"],
|
||||
"enabled": True,
|
||||
}
|
||||
for acc in res
|
||||
acc["steamid"]: acc["personaname"] for acc in res
|
||||
}
|
||||
}
|
||||
user_input.pop(CONF_ACCOUNTS)
|
||||
else:
|
||||
accounts_data = {
|
||||
CONF_ACCOUNTS: {
|
||||
user_input[CONF_ACCOUNT]: {"name": name, "enabled": True}
|
||||
}
|
||||
}
|
||||
accounts_data = {CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}}
|
||||
return self.async_create_entry(
|
||||
title=name or DEFAULT_NAME,
|
||||
data=user_input,
|
||||
@@ -103,6 +102,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=PLACEHOLDERS,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config: ConfigType) -> FlowResult:
|
||||
@@ -111,7 +111,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if entry.data[CONF_API_KEY] == import_config[CONF_API_KEY]:
|
||||
return self.async_abort(reason="already_configured")
|
||||
LOGGER.warning(
|
||||
"Steam yaml config in now deprecated and has been imported. "
|
||||
"Steam yaml config is now deprecated and has been imported. "
|
||||
"Please remove it from your config"
|
||||
)
|
||||
import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0]
|
||||
@@ -131,7 +131,9 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_user()
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", description_placeholders=PLACEHOLDERS
|
||||
)
|
||||
|
||||
|
||||
class SteamOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
@@ -148,56 +150,38 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Manage Steam options."""
|
||||
if user_input is not None:
|
||||
await self.hass.config_entries.async_unload(self.entry.entry_id)
|
||||
for k in self.options[CONF_ACCOUNTS]:
|
||||
if (
|
||||
self.options[CONF_ACCOUNTS][k]["enabled"]
|
||||
and k not in user_input[CONF_ACCOUNTS]
|
||||
and (
|
||||
entity_id := er.async_get(self.hass).async_get_entity_id(
|
||||
Platform.SENSOR, DOMAIN, f"sensor.steam_{k}"
|
||||
)
|
||||
for _id in self.options[CONF_ACCOUNTS]:
|
||||
if _id not in user_input[CONF_ACCOUNTS] and (
|
||||
entity_id := er.async_get(self.hass).async_get_entity_id(
|
||||
Platform.SENSOR, DOMAIN, f"sensor.steam_{_id}"
|
||||
)
|
||||
):
|
||||
er.async_get(self.hass).async_remove(entity_id)
|
||||
channel_data = {
|
||||
CONF_ACCOUNTS: {
|
||||
k: {
|
||||
"name": v["name"],
|
||||
"enabled": k in user_input[CONF_ACCOUNTS],
|
||||
}
|
||||
for k, v in self.options[CONF_ACCOUNTS].items()
|
||||
if k in user_input[CONF_ACCOUNTS]
|
||||
_id: name
|
||||
for _id, name in self.options[CONF_ACCOUNTS].items()
|
||||
if _id in user_input[CONF_ACCOUNTS]
|
||||
}
|
||||
}
|
||||
await self.hass.config_entries.async_reload(self.entry.entry_id)
|
||||
return self.async_create_entry(title="", data=channel_data)
|
||||
try:
|
||||
users = {
|
||||
name["steamid"]: {"name": name["personaname"], "enabled": False}
|
||||
name["steamid"]: name["personaname"]
|
||||
for name in await self.hass.async_add_executor_job(self.get_accounts)
|
||||
}
|
||||
|
||||
except steam.api.HTTPTimeoutError:
|
||||
users = self.options[CONF_ACCOUNTS]
|
||||
_users = users | self.options[CONF_ACCOUNTS]
|
||||
self.options[CONF_ACCOUNTS] = {
|
||||
k: v
|
||||
for k, v in _users.items()
|
||||
if k in users or self.options[CONF_ACCOUNTS][k]["enabled"]
|
||||
}
|
||||
|
||||
options = {
|
||||
vol.Required(
|
||||
CONF_ACCOUNTS,
|
||||
default={
|
||||
k
|
||||
for k in self.options[CONF_ACCOUNTS]
|
||||
if self.options[CONF_ACCOUNTS][k]["enabled"]
|
||||
},
|
||||
): cv.multi_select(
|
||||
{k: v["name"] for k, v in self.options[CONF_ACCOUNTS].items()}
|
||||
),
|
||||
default=set(self.options[CONF_ACCOUNTS]),
|
||||
): cv.multi_select(users | self.options[CONF_ACCOUNTS]),
|
||||
}
|
||||
self.options[CONF_ACCOUNTS] = users | self.options[CONF_ACCOUNTS]
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
||||
|
||||
@@ -205,7 +189,6 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Get accounts."""
|
||||
interface = steam.api.interface("ISteamUser")
|
||||
friends = interface.GetFriendList(steamid=self.entry.data[CONF_ACCOUNT])
|
||||
friends = friends["friendslist"]["friends"]
|
||||
_users_str = [user["steamid"] for user in friends]
|
||||
_users_str = [user["steamid"] for user in friends["friendslist"]["friends"]]
|
||||
names = interface.GetPlayerSummaries(steamids=_users_str)
|
||||
return names["response"]["players"]["player"]
|
||||
|
||||
@@ -11,6 +11,11 @@ DOMAIN: Final = "steam_online"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
PLACEHOLDERS = {
|
||||
"api_key_url": "https://steamcommunity.com/dev/apikey",
|
||||
"account_id_url": "https://steamid.io",
|
||||
}
|
||||
|
||||
STATE_OFFLINE = "offline"
|
||||
STATE_ONLINE = "online"
|
||||
STATE_BUSY = "busy"
|
||||
@@ -30,6 +35,4 @@ STEAM_STATUSES = {
|
||||
STEAM_API_URL = "https://steamcdn-a.akamaihd.net/steam/apps/"
|
||||
STEAM_HEADER_IMAGE_FILE = "header.jpg"
|
||||
STEAM_MAIN_IMAGE_FILE = "capsule_616x353.jpg"
|
||||
STEAM_ICON_URL = (
|
||||
"https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/%d/%s.jpg"
|
||||
)
|
||||
STEAM_ICON_URL = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/"
|
||||
|
||||
@@ -28,7 +28,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
self.game_icons: dict = {}
|
||||
self.game_icons: dict[int, str] = {}
|
||||
self.player_interface: INTMethod = None
|
||||
self.user_interface: INTMethod = None
|
||||
steam.api.key.set(self.config_entry.data[CONF_API_KEY])
|
||||
@@ -36,7 +36,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
def _update(self) -> dict[str, dict[str, str | int]]:
|
||||
"""Fetch data from API endpoint."""
|
||||
accounts = self.config_entry.options[CONF_ACCOUNTS]
|
||||
_ids = [k for k in accounts if accounts[k]["enabled"]]
|
||||
_ids = list(accounts)
|
||||
if not self.user_interface or not self.player_interface:
|
||||
self.user_interface = steam.api.interface("ISteamUser")
|
||||
self.player_interface = steam.api.interface("IPlayerService")
|
||||
@@ -46,7 +46,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
steamid=_id, include_appinfo=1
|
||||
)["response"]
|
||||
self.game_icons = self.game_icons | {
|
||||
game["appid"]: game["img_icon_url"] for game in res.get("games", {})
|
||||
game["appid"]: game["img_icon_url"] for game in res.get("games", [])
|
||||
}
|
||||
response = self.user_interface.GetPlayerSummaries(steamids=_ids)
|
||||
players = {
|
||||
@@ -56,8 +56,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
}
|
||||
for k in players:
|
||||
data = self.player_interface.GetSteamLevel(steamid=players[k]["steamid"])
|
||||
data = data["response"]
|
||||
players[k]["level"] = data["player_level"]
|
||||
players[k]["level"] = data["response"]["player_level"]
|
||||
return players
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, str | int]]:
|
||||
|
||||
@@ -65,7 +65,6 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
SteamSensor(hass.data[DOMAIN][entry.entry_id], account)
|
||||
for account in entry.options[CONF_ACCOUNTS]
|
||||
if entry.options[CONF_ACCOUNTS][account]["enabled"]
|
||||
)
|
||||
|
||||
|
||||
@@ -106,10 +105,7 @@ class SteamSensor(SteamEntity, SensorEntity):
|
||||
attrs["game_image_header"] = f"{game_url}{STEAM_HEADER_IMAGE_FILE}"
|
||||
attrs["game_image_main"] = f"{game_url}{STEAM_MAIN_IMAGE_FILE}"
|
||||
if info := self._get_game_icon(player):
|
||||
attrs["game_icon"] = STEAM_ICON_URL % (
|
||||
game_id,
|
||||
info,
|
||||
)
|
||||
attrs["game_icon"] = f"{STEAM_ICON_URL}{game_id}/{info}.jpg"
|
||||
self._attr_name = player["personaname"]
|
||||
self._attr_entity_picture = player["avatarmedium"]
|
||||
if last_online := player.get("lastlogoff"):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Use https://steamid.io to find your Steam account ID",
|
||||
"description": "Use {account_id_url} to find your Steam account ID",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"account": "Steam account ID"
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey"
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: {api_key_url}"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey",
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: {api_key_url}",
|
||||
"title": "Reauthenticate Integration"
|
||||
},
|
||||
"user": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"account": "Steam account ID",
|
||||
"api_key": "API Key"
|
||||
},
|
||||
"description": "Use https://steamid.io to find your Steam account ID"
|
||||
"description": "Use {account_id_url} to find your Steam account ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,5 +74,5 @@ class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the station is open."""
|
||||
data = self.coordinator.data[self._station_id]
|
||||
return data is not None and "status" in data
|
||||
data: dict = self.coordinator.data[self._station_id]
|
||||
return data is not None and data.get("status") == "open"
|
||||
|
||||
@@ -207,8 +207,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity):
|
||||
return
|
||||
|
||||
_LOGGER.error(
|
||||
"Received invalid alarm panel state: %s. Expected: %s",
|
||||
"Received invalid alarm panel state: %s for entity %s. Expected: %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
|
||||
@@ -222,8 +222,9 @@ class CoverTemplate(TemplateEntity, CoverEntity):
|
||||
self._is_closing = state == STATE_CLOSING
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid cover is_on state: %s. Expected: %s",
|
||||
"Received invalid cover is_on state: %s for entity %s. Expected: %s",
|
||||
state,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
if not self._position_template:
|
||||
|
||||
@@ -285,8 +285,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
"""Set the preset_mode of the fan."""
|
||||
if self.preset_modes and preset_mode not in self.preset_modes:
|
||||
_LOGGER.error(
|
||||
"Received invalid preset_mode: %s. Expected: %s",
|
||||
"Received invalid preset_mode: %s for entity %s. Expected: %s",
|
||||
preset_mode,
|
||||
self.entity_id,
|
||||
self.preset_modes,
|
||||
)
|
||||
return
|
||||
@@ -322,8 +323,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid direction: %s. Expected: %s",
|
||||
"Received invalid direction: %s for entity %s. Expected: %s",
|
||||
direction,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_DIRECTIONS),
|
||||
)
|
||||
|
||||
@@ -341,8 +343,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
self._state = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid fan is_on state: %s. Expected: %s",
|
||||
"Received invalid fan is_on state: %s for entity %s. Expected: %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
@@ -390,7 +393,11 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
try:
|
||||
percentage = int(float(percentage))
|
||||
except ValueError:
|
||||
_LOGGER.error("Received invalid percentage: %s", percentage)
|
||||
_LOGGER.error(
|
||||
"Received invalid percentage: %s for entity %s",
|
||||
percentage,
|
||||
self.entity_id,
|
||||
)
|
||||
self._percentage = 0
|
||||
self._preset_mode = None
|
||||
return
|
||||
@@ -399,7 +406,11 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
self._percentage = percentage
|
||||
self._preset_mode = None
|
||||
else:
|
||||
_LOGGER.error("Received invalid percentage: %s", percentage)
|
||||
_LOGGER.error(
|
||||
"Received invalid percentage: %s for entity %s",
|
||||
percentage,
|
||||
self.entity_id,
|
||||
)
|
||||
self._percentage = 0
|
||||
self._preset_mode = None
|
||||
|
||||
@@ -416,8 +427,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
self._preset_mode = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid preset_mode: %s. Expected: %s",
|
||||
"Received invalid preset_mode: %s for entity %s. Expected: %s",
|
||||
preset_mode,
|
||||
self.entity_id,
|
||||
self.preset_mode,
|
||||
)
|
||||
self._percentage = None
|
||||
@@ -434,8 +446,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
self._oscillating = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid oscillating: %s. Expected: True/False",
|
||||
"Received invalid oscillating: %s for entity %s. Expected: True/False",
|
||||
oscillating,
|
||||
self.entity_id,
|
||||
)
|
||||
self._oscillating = None
|
||||
|
||||
@@ -448,8 +461,9 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
self._direction = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid direction: %s. Expected: %s",
|
||||
"Received invalid direction: %s for entity %s. Expected: %s",
|
||||
direction,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_DIRECTIONS),
|
||||
)
|
||||
self._direction = None
|
||||
|
||||
@@ -393,8 +393,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
effect = kwargs[ATTR_EFFECT]
|
||||
if effect not in self._effect_list:
|
||||
_LOGGER.error(
|
||||
"Received invalid effect: %s. Expected one of: %s",
|
||||
"Received invalid effect: %s for entity %s. Expected one of: %s",
|
||||
effect,
|
||||
self.entity_id,
|
||||
self._effect_list,
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -443,7 +444,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._brightness = int(brightness)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid brightness : %s. Expected: 0-255", brightness
|
||||
"Received invalid brightness : %s for entity %s. Expected: 0-255",
|
||||
brightness,
|
||||
self.entity_id,
|
||||
)
|
||||
self._brightness = None
|
||||
except ValueError:
|
||||
@@ -464,7 +467,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._white_value = int(white_value)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid white value: %s. Expected: 0-255", white_value
|
||||
"Received invalid white value: %s for entity %s. Expected: 0-255",
|
||||
white_value,
|
||||
self.entity_id,
|
||||
)
|
||||
self._white_value = None
|
||||
except ValueError:
|
||||
@@ -483,8 +488,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
|
||||
if not isinstance(effect_list, list):
|
||||
_LOGGER.error(
|
||||
"Received invalid effect list: %s. Expected list of strings",
|
||||
"Received invalid effect list: %s for entity %s. Expected list of strings",
|
||||
effect_list,
|
||||
self.entity_id,
|
||||
)
|
||||
self._effect_list = None
|
||||
return
|
||||
@@ -504,8 +510,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
|
||||
if effect not in self._effect_list:
|
||||
_LOGGER.error(
|
||||
"Received invalid effect: %s. Expected one of: %s",
|
||||
"Received invalid effect: %s for entity %s. Expected one of: %s",
|
||||
effect,
|
||||
self.entity_id,
|
||||
self._effect_list,
|
||||
)
|
||||
self._effect = None
|
||||
@@ -533,8 +540,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
return
|
||||
|
||||
_LOGGER.error(
|
||||
"Received invalid light is_on state: %s. Expected: %s",
|
||||
"Received invalid light is_on state: %s for entity %s. Expected: %s",
|
||||
state,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
@@ -551,8 +559,9 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._temperature = temperature
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid color temperature : %s. Expected: %s-%s",
|
||||
"Received invalid color temperature : %s for entity %s. Expected: %s-%s",
|
||||
temperature,
|
||||
self.entity_id,
|
||||
self.min_mireds,
|
||||
self.max_mireds,
|
||||
)
|
||||
@@ -591,13 +600,16 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._color = (h_str, s_str)
|
||||
elif h_str is not None and s_str is not None:
|
||||
_LOGGER.error(
|
||||
"Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)",
|
||||
"Received invalid hs_color : (%s, %s) for entity %s. Expected: (0-360, 0-100)",
|
||||
h_str,
|
||||
s_str,
|
||||
self.entity_id,
|
||||
)
|
||||
self._color = None
|
||||
else:
|
||||
_LOGGER.error("Received invalid hs_color : (%s)", render)
|
||||
_LOGGER.error(
|
||||
"Received invalid hs_color : (%s) for entity %s", render, self.entity_id
|
||||
)
|
||||
self._color = None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -253,8 +253,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid fan speed: %s. Expected: %s",
|
||||
"Received invalid fan speed: %s for entity %s. Expected: %s",
|
||||
fan_speed,
|
||||
self.entity_id,
|
||||
self._attr_fan_speed_list,
|
||||
)
|
||||
|
||||
@@ -298,8 +299,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||
self._state = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid vacuum state: %s. Expected: %s",
|
||||
"Received invalid vacuum state: %s for entity %s. Expected: %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
@@ -312,7 +314,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Received invalid battery level: %s. Expected: 0-100", battery_level
|
||||
"Received invalid battery level: %s for entity %s. Expected: 0-100",
|
||||
battery_level,
|
||||
self.entity_id,
|
||||
)
|
||||
self._attr_battery_level = None
|
||||
return
|
||||
@@ -333,8 +337,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||
self._attr_fan_speed = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid fan speed: %s. Expected: %s",
|
||||
"Received invalid fan speed: %s for entity %s. Expected: %s",
|
||||
fan_speed,
|
||||
self.entity_id,
|
||||
self._attr_fan_speed_list,
|
||||
)
|
||||
self._attr_fan_speed = None
|
||||
|
||||
@@ -256,6 +256,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the UniFi Network options."""
|
||||
if self.config_entry.entry_id not in self.hass.data[UNIFI_DOMAIN]:
|
||||
return self.async_abort(reason="integration_not_setup")
|
||||
self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id]
|
||||
self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ def add_client_entities(controller, async_add_entities, clients):
|
||||
trackers = []
|
||||
|
||||
for mac in clients:
|
||||
if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE]:
|
||||
if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE] or not (
|
||||
client := controller.api.clients.get(mac)
|
||||
):
|
||||
continue
|
||||
|
||||
client = controller.api.clients[mac]
|
||||
|
||||
if mac not in controller.wireless_clients:
|
||||
if not controller.option_track_wired_clients:
|
||||
continue
|
||||
|
||||
@@ -21,11 +21,14 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "UniFi Network site is already configured",
|
||||
"configuration_updated": "Configuration updated.",
|
||||
"configuration_updated": "Configuration updated",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"integration_not_setup": "UniFi integration is not setup"
|
||||
},
|
||||
"step": {
|
||||
"device_tracker": {
|
||||
"data": {
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"integration_not_setup": "UniFi integration is not setup"
|
||||
},
|
||||
"step": {
|
||||
"client_control": {
|
||||
"data": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.38.4"],
|
||||
"requirements": ["zeroconf==0.38.5"],
|
||||
"dependencies": ["network", "api"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
|
||||
@@ -345,7 +345,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque
|
||||
"""Frequency measurement."""
|
||||
|
||||
SENSOR_ATTR = "ac_frequency"
|
||||
_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
|
||||
_unit = FREQUENCY_HERTZ
|
||||
_div_mul_prefix = "ac_frequency"
|
||||
|
||||
@@ -360,7 +360,7 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f
|
||||
"""Frequency measurement."""
|
||||
|
||||
SENSOR_ATTR = "power_factor"
|
||||
_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
|
||||
_unit = PERCENTAGE
|
||||
|
||||
@property
|
||||
@@ -718,8 +718,8 @@ class SinopeHVACAction(ThermostatHVACAction):
|
||||
class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
"""RSSI sensor for a device."""
|
||||
|
||||
_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
|
||||
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.36.0"],
|
||||
"requirements": ["zwave-js-server-python==0.36.1"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["usb", "http", "websocket_api"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -951,8 +951,9 @@ def async_notify_setup_error(
|
||||
message = "The following integrations and platforms could not be set up:\n\n"
|
||||
|
||||
for name, link in errors.items():
|
||||
show_logs = f"[Show logs](/config/logs?filter={name})"
|
||||
part = f"[{name}]({link})" if link else name
|
||||
message += f" - {part}\n"
|
||||
message += f" - {part} ({show_logs})\n"
|
||||
|
||||
message += "\nPlease check your config and [logs](/config/logs)."
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "0b4"
|
||||
PATCH_VERSION: Final = "0b6"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
@@ -764,11 +764,9 @@ CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"]
|
||||
# use the EntityCategory enum instead.
|
||||
ENTITY_CATEGORY_CONFIG: Final = "config"
|
||||
ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic"
|
||||
ENTITY_CATEGORY_SYSTEM: Final = "system"
|
||||
ENTITY_CATEGORIES: Final[list[str]] = [
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ENTITY_CATEGORY_SYSTEM,
|
||||
]
|
||||
|
||||
# The ID of the Home Assistant Media Player Cast App
|
||||
|
||||
@@ -192,18 +192,15 @@ class HassJob(Generic[_R_co]):
|
||||
|
||||
def __init__(self, target: Callable[..., _R_co]) -> None:
|
||||
"""Create a job object."""
|
||||
if asyncio.iscoroutine(target):
|
||||
raise ValueError("Coroutine not allowed to be passed to HassJob")
|
||||
|
||||
self.target = target
|
||||
self.job_type = _get_callable_job_type(target)
|
||||
self.job_type = _get_hassjob_callable_job_type(target)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return the job."""
|
||||
return f"<Job {self.job_type} {self.target}>"
|
||||
|
||||
|
||||
def _get_callable_job_type(target: Callable[..., Any]) -> HassJobType:
|
||||
def _get_hassjob_callable_job_type(target: Callable[..., Any]) -> HassJobType:
|
||||
"""Determine the job type from the callable."""
|
||||
# Check for partials to properly determine if coroutine function
|
||||
check_target = target
|
||||
@@ -214,6 +211,8 @@ def _get_callable_job_type(target: Callable[..., Any]) -> HassJobType:
|
||||
return HassJobType.Coroutinefunction
|
||||
if is_callback(check_target):
|
||||
return HassJobType.Callback
|
||||
if asyncio.iscoroutine(check_target):
|
||||
raise ValueError("Coroutine not allowed to be passed to HassJob")
|
||||
return HassJobType.Executor
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ SSDP = {
|
||||
],
|
||||
"deconz": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
"manufacturerURL": "http://www.dresden-elektronik.de"
|
||||
}
|
||||
],
|
||||
"denonavr": [
|
||||
|
||||
@@ -200,9 +200,6 @@ class EntityCategory(StrEnum):
|
||||
# Diagnostic: An entity exposing some configuration parameter or diagnostics of a device
|
||||
DIAGNOSTIC = "diagnostic"
|
||||
|
||||
# System: An entity which is not useful for the user to interact with
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory)
|
||||
|
||||
|
||||
@@ -519,8 +519,8 @@ class EntityPlatform:
|
||||
config_entry=self.config_entry,
|
||||
device_id=device_id,
|
||||
disabled_by=disabled_by,
|
||||
hidden_by=hidden_by,
|
||||
entity_category=entity.entity_category,
|
||||
hidden_by=hidden_by,
|
||||
known_object_ids=self.entities.keys(),
|
||||
original_device_class=entity.device_class,
|
||||
original_icon=entity.icon,
|
||||
|
||||
@@ -395,8 +395,14 @@ class _ScriptRun:
|
||||
script_execution_set("finished")
|
||||
except _StopScript:
|
||||
script_execution_set("finished")
|
||||
# Let the _StopScript bubble up if this is a sub-script
|
||||
if not self._script.top_level:
|
||||
raise
|
||||
except _AbortScript:
|
||||
script_execution_set("aborted")
|
||||
# Let the _AbortScript bubble up if this is a sub-script
|
||||
if not self._script.top_level:
|
||||
raise
|
||||
except Exception:
|
||||
script_execution_set("error")
|
||||
raise
|
||||
@@ -1143,7 +1149,7 @@ class Script:
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass)
|
||||
)
|
||||
self._top_level = top_level
|
||||
self.top_level = top_level
|
||||
if top_level:
|
||||
all_scripts.append(
|
||||
{"instance": self, "started_before_shutdown": not hass.is_stopping}
|
||||
@@ -1431,7 +1437,7 @@ class Script:
|
||||
# If this is a top level Script then make a copy of the variables in case they
|
||||
# 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:
|
||||
if self.top_level:
|
||||
if self.variables:
|
||||
try:
|
||||
variables = self.variables.async_render(
|
||||
|
||||
@@ -20,8 +20,19 @@ def async_at_start(
|
||||
hass.async_run_hass_job(at_start_job, hass)
|
||||
return lambda: None
|
||||
|
||||
async def _matched_event(event: Event) -> None:
|
||||
unsub: None | CALLBACK_TYPE = None
|
||||
|
||||
@callback
|
||||
def _matched_event(event: Event) -> None:
|
||||
"""Call the callback when Home Assistant started."""
|
||||
hass.async_run_hass_job(at_start_job, hass)
|
||||
nonlocal unsub
|
||||
unsub = None
|
||||
|
||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event)
|
||||
@callback
|
||||
def cancel() -> None:
|
||||
if unsub:
|
||||
unsub()
|
||||
|
||||
unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event)
|
||||
return cancel
|
||||
|
||||
@@ -1331,7 +1331,7 @@ def warn_no_default(function, value, default):
|
||||
(
|
||||
"Template warning: '%s' got invalid input '%s' when %s template '%s' "
|
||||
"but no default was specified. Currently '%s' will return '%s', however this template will fail "
|
||||
"to render in Home Assistant core 2022.1"
|
||||
"to render in Home Assistant core 2022.6"
|
||||
),
|
||||
function,
|
||||
value,
|
||||
|
||||
@@ -15,7 +15,7 @@ ciso8601==2.2.0
|
||||
cryptography==36.0.2
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.54.0
|
||||
home-assistant-frontend==20220429.0
|
||||
home-assistant-frontend==20220502.0
|
||||
httpx==0.22.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.1
|
||||
@@ -34,7 +34,7 @@ typing-extensions>=3.10.0.2,<5.0
|
||||
voluptuous-serialize==2.5.0
|
||||
voluptuous==0.13.1
|
||||
yarl==1.7.2
|
||||
zeroconf==0.38.4
|
||||
zeroconf==0.38.5
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
@@ -689,7 +689,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==0.6.3
|
||||
gcal-sync==0.7.1
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -819,7 +819,7 @@ hole==0.7.0
|
||||
holidays==0.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220429.0
|
||||
home-assistant-frontend==20220502.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.0
|
||||
@@ -1547,7 +1547,7 @@ pyialarm==1.9.0
|
||||
pyicloud==1.0.0
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.1.0b3
|
||||
pyinsteon==1.1.0
|
||||
|
||||
# homeassistant.components.intesishome
|
||||
pyintesishome==1.7.6
|
||||
@@ -1783,7 +1783,7 @@ pysaj==0.0.16
|
||||
pysdcp==1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.12
|
||||
pysensibo==1.0.14
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@@ -1934,7 +1934,7 @@ python-qbittorrent==0.4.2
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.31
|
||||
python-smarttub==0.0.32
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.14.1
|
||||
@@ -2153,7 +2153,7 @@ simplehound==0.3
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.04.1
|
||||
simplisafe-python==2022.05.0
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==3.1.2
|
||||
@@ -2445,7 +2445,7 @@ xbox-webapi==2.0.11
|
||||
xboxapi==2.0.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==0.20.4
|
||||
xknx==0.21.1
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@@ -2480,7 +2480,7 @@ youtube_dl==2021.12.17
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.38.4
|
||||
zeroconf==0.38.5
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.73
|
||||
@@ -2510,7 +2510,7 @@ zigpy==0.45.1
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.36.0
|
||||
zwave-js-server-python==0.36.1
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.2.4
|
||||
|
||||
@@ -486,7 +486,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==0.6.3
|
||||
gcal-sync==0.7.1
|
||||
|
||||
# homeassistant.components.usgs_earthquakes_feed
|
||||
geojson_client==0.6
|
||||
@@ -580,7 +580,7 @@ hole==0.7.0
|
||||
holidays==0.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220429.0
|
||||
home-assistant-frontend==20220502.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.0
|
||||
@@ -1029,7 +1029,7 @@ pyialarm==1.9.0
|
||||
pyicloud==1.0.0
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.1.0b3
|
||||
pyinsteon==1.1.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
@@ -1196,7 +1196,7 @@ pyruckus==0.12
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.12
|
||||
pysensibo==1.0.14
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@@ -1269,7 +1269,7 @@ python-nest==4.2.0
|
||||
python-picnic-api==1.1.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.31
|
||||
python-smarttub==0.0.32
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.14.1
|
||||
@@ -1404,7 +1404,7 @@ sharkiq==0.0.1
|
||||
simplehound==0.3
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.04.1
|
||||
simplisafe-python==2022.05.0
|
||||
|
||||
# homeassistant.components.slack
|
||||
slackclient==2.5.0
|
||||
@@ -1597,7 +1597,7 @@ wolf_smartset==0.1.11
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==0.20.4
|
||||
xknx==0.21.1
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@@ -1620,7 +1620,7 @@ yeelight==0.7.10
|
||||
youless-api==0.16
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.38.4
|
||||
zeroconf==0.38.5
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.73
|
||||
@@ -1641,7 +1641,7 @@ zigpy-znp==0.7.0
|
||||
zigpy==0.45.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.36.0
|
||||
zwave-js-server-python==0.36.1
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.2.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.5.0b4
|
||||
version = 2022.5.0b6
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
||||
@@ -43,20 +43,13 @@ async def test_categorized_hidden_entities(hass):
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
entity_entry3 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_system_id",
|
||||
suggested_object_id="system_switch",
|
||||
entity_category=EntityCategory.SYSTEM,
|
||||
)
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_hidden_integration_id",
|
||||
suggested_object_id="hidden_integration_switch",
|
||||
hidden_by=er.RegistryEntryHider.INTEGRATION,
|
||||
)
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_hidden_user_id",
|
||||
@@ -69,7 +62,6 @@ async def test_categorized_hidden_entities(hass):
|
||||
hass.states.async_set(entity_entry2.entity_id, "something_else")
|
||||
hass.states.async_set(entity_entry3.entity_id, "blah")
|
||||
hass.states.async_set(entity_entry4.entity_id, "foo")
|
||||
hass.states.async_set(entity_entry5.entity_id, "bar")
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, get_default_config(hass), request)
|
||||
|
||||
|
||||
@@ -1079,6 +1079,7 @@ async def test_automation_restore_last_triggered_with_initial_state(hass):
|
||||
|
||||
async def test_extraction_functions(hass):
|
||||
"""Test extraction functions."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -1086,7 +1087,24 @@ async def test_extraction_functions(hass):
|
||||
DOMAIN: [
|
||||
{
|
||||
"alias": "test1",
|
||||
"trigger": {"platform": "state", "entity_id": "sensor.trigger_1"},
|
||||
"trigger": [
|
||||
{"platform": "state", "entity_id": "sensor.trigger_state"},
|
||||
{
|
||||
"platform": "numeric_state",
|
||||
"entity_id": "sensor.trigger_numeric_state",
|
||||
"above": 10,
|
||||
},
|
||||
{
|
||||
"platform": "calendar",
|
||||
"entity_id": "calendar.trigger_calendar",
|
||||
"event": "start",
|
||||
},
|
||||
{
|
||||
"platform": "event",
|
||||
"event_type": "state_changed",
|
||||
"event_data": {"entity_id": "sensor.trigger_event"},
|
||||
},
|
||||
],
|
||||
"condition": {
|
||||
"condition": "state",
|
||||
"entity_id": "light.condition_state",
|
||||
@@ -1111,13 +1129,30 @@ async def test_extraction_functions(hass):
|
||||
},
|
||||
{
|
||||
"alias": "test2",
|
||||
"trigger": {
|
||||
"platform": "device",
|
||||
"domain": "light",
|
||||
"type": "turned_on",
|
||||
"entity_id": "light.trigger_2",
|
||||
"device_id": "trigger-device-2",
|
||||
},
|
||||
"trigger": [
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": "light",
|
||||
"type": "turned_on",
|
||||
"entity_id": "light.trigger_2",
|
||||
"device_id": "trigger-device-2",
|
||||
},
|
||||
{
|
||||
"platform": "tag",
|
||||
"tag_id": "1234",
|
||||
"device_id": "device-trigger-tag1",
|
||||
},
|
||||
{
|
||||
"platform": "tag",
|
||||
"tag_id": "1234",
|
||||
"device_id": ["device-trigger-tag2", "device-trigger-tag3"],
|
||||
},
|
||||
{
|
||||
"platform": "event",
|
||||
"event_type": "esphome.button_pressed",
|
||||
"event_data": {"device_id": "device-trigger-event"},
|
||||
},
|
||||
],
|
||||
"condition": {
|
||||
"condition": "device",
|
||||
"device_id": "condition-device",
|
||||
@@ -1159,7 +1194,10 @@ async def test_extraction_functions(hass):
|
||||
"automation.test2",
|
||||
}
|
||||
assert set(automation.entities_in_automation(hass, "automation.test1")) == {
|
||||
"sensor.trigger_1",
|
||||
"calendar.trigger_calendar",
|
||||
"sensor.trigger_state",
|
||||
"sensor.trigger_numeric_state",
|
||||
"sensor.trigger_event",
|
||||
"light.condition_state",
|
||||
"light.in_both",
|
||||
"light.in_first",
|
||||
@@ -1173,6 +1211,10 @@ async def test_extraction_functions(hass):
|
||||
"condition-device",
|
||||
"device-in-both",
|
||||
"device-in-last",
|
||||
"device-trigger-event",
|
||||
"device-trigger-tag1",
|
||||
"device-trigger-tag2",
|
||||
"device-trigger-tag3",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,20 +39,13 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub):
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
entity_entry3 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_system_id",
|
||||
suggested_object_id="system_light",
|
||||
entity_category=EntityCategory.SYSTEM,
|
||||
)
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_hidden_integration_id",
|
||||
suggested_object_id="hidden_integration_light",
|
||||
hidden_by=er.RegistryEntryHider.INTEGRATION,
|
||||
)
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_hidden_user_id",
|
||||
@@ -77,7 +70,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub):
|
||||
assert not conf.should_expose(entity_entry2.entity_id)
|
||||
assert not conf.should_expose(entity_entry3.entity_id)
|
||||
assert not conf.should_expose(entity_entry4.entity_id)
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
entity_conf["should_expose"] = True
|
||||
assert conf.should_expose("light.kitchen")
|
||||
@@ -86,7 +78,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub):
|
||||
assert not conf.should_expose(entity_entry2.entity_id)
|
||||
assert not conf.should_expose(entity_entry3.entity_id)
|
||||
assert not conf.should_expose(entity_entry4.entity_id)
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
entity_conf["should_expose"] = None
|
||||
assert conf.should_expose("light.kitchen")
|
||||
@@ -95,7 +86,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub):
|
||||
assert not conf.should_expose(entity_entry2.entity_id)
|
||||
assert not conf.should_expose(entity_entry3.entity_id)
|
||||
assert not conf.should_expose(entity_entry4.entity_id)
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
assert "alexa" not in hass.config.components
|
||||
await cloud_prefs.async_update(
|
||||
|
||||
@@ -298,20 +298,13 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs):
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
entity_entry3 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_system_id",
|
||||
suggested_object_id="system_light",
|
||||
entity_category=EntityCategory.SYSTEM,
|
||||
)
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_hidden_integration_id",
|
||||
suggested_object_id="hidden_integration_light",
|
||||
hidden_by=er.RegistryEntryHider.INTEGRATION,
|
||||
)
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_hidden_user_id",
|
||||
@@ -328,14 +321,12 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs):
|
||||
state = State("light.kitchen", "on")
|
||||
state_config = State(entity_entry1.entity_id, "on")
|
||||
state_diagnostic = State(entity_entry2.entity_id, "on")
|
||||
state_system = State(entity_entry3.entity_id, "on")
|
||||
state_hidden_integration = State(entity_entry4.entity_id, "on")
|
||||
state_hidden_user = State(entity_entry5.entity_id, "on")
|
||||
state_hidden_integration = State(entity_entry3.entity_id, "on")
|
||||
state_hidden_user = State(entity_entry4.entity_id, "on")
|
||||
|
||||
assert not mock_conf.should_expose(state)
|
||||
assert not mock_conf.should_expose(state_config)
|
||||
assert not mock_conf.should_expose(state_diagnostic)
|
||||
assert not mock_conf.should_expose(state_system)
|
||||
assert not mock_conf.should_expose(state_hidden_integration)
|
||||
assert not mock_conf.should_expose(state_hidden_user)
|
||||
|
||||
@@ -344,7 +335,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs):
|
||||
# categorized and hidden entities should not be exposed
|
||||
assert not mock_conf.should_expose(state_config)
|
||||
assert not mock_conf.should_expose(state_diagnostic)
|
||||
assert not mock_conf.should_expose(state_system)
|
||||
assert not mock_conf.should_expose(state_hidden_integration)
|
||||
assert not mock_conf.should_expose(state_hidden_user)
|
||||
|
||||
@@ -353,7 +343,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs):
|
||||
# categorized and hidden entities should not be exposed
|
||||
assert not mock_conf.should_expose(state_config)
|
||||
assert not mock_conf.should_expose(state_diagnostic)
|
||||
assert not mock_conf.should_expose(state_system)
|
||||
assert not mock_conf.should_expose(state_hidden_integration)
|
||||
assert not mock_conf.should_expose(state_hidden_user)
|
||||
|
||||
|
||||
@@ -6,21 +6,34 @@ import pytest
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components import config
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_script(hass, stub_blueprint_populate): # noqa: F811
|
||||
async def setup_script(hass, script_config, stub_blueprint_populate): # noqa: F811
|
||||
"""Set up script integration."""
|
||||
assert await async_setup_component(hass, "script", {})
|
||||
assert await async_setup_component(hass, "script", {"script": script_config})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"script_config",
|
||||
(
|
||||
{
|
||||
"one": {"alias": "Light on", "sequence": []},
|
||||
"two": {"alias": "Light off", "sequence": []},
|
||||
},
|
||||
),
|
||||
)
|
||||
async def test_delete_script(hass, hass_client):
|
||||
"""Test deleting a script."""
|
||||
with patch.object(config, "SECTIONS", ["script"]):
|
||||
await async_setup_component(hass, "config", {})
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
assert len(ent_reg.entities) == 2
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
orig_data = {"one": {}, "two": {}}
|
||||
@@ -46,3 +59,5 @@ async def test_delete_script(hass, hass_client):
|
||||
|
||||
assert len(written) == 1
|
||||
assert written[0] == {"one": {}}
|
||||
|
||||
assert len(ent_reg.entities) == 1
|
||||
|
||||
@@ -459,22 +459,6 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
|
||||
}
|
||||
|
||||
|
||||
async def test_flow_ssdp_bad_discovery(hass, aioclient_mock):
|
||||
"""Test that SSDP discovery aborts if manufacturer URL is wrong."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DECONZ_DOMAIN,
|
||||
data=ssdp.SsdpServiceInfo(
|
||||
ssdp_usn="mock_usn",
|
||||
ssdp_st="mock_st",
|
||||
upnp={ATTR_UPNP_MANUFACTURER_URL: "other"},
|
||||
),
|
||||
context={"source": SOURCE_SSDP},
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "not_deconz_bridge"
|
||||
|
||||
|
||||
async def test_ssdp_discovery_update_configuration(hass, aioclient_mock):
|
||||
"""Test if a discovered bridge is configured but updates with new attributes."""
|
||||
config_entry = await setup_deconz_integration(hass, aioclient_mock)
|
||||
|
||||
@@ -785,6 +785,36 @@ async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket):
|
||||
assert hass.states.get("sensor.light_level_sensor").state == "999.8"
|
||||
|
||||
|
||||
BAD_SENSOR_DATA = [
|
||||
("ZHAConsumption", "consumption"),
|
||||
("ZHAHumidity", "humidity"),
|
||||
("ZHALightLevel", "lightlevel"),
|
||||
("ZHATemperature", "temperature"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sensor_type, sensor_property", BAD_SENSOR_DATA)
|
||||
async def test_dont_add_sensor_if_state_is_none(
|
||||
hass, aioclient_mock, sensor_type, sensor_property
|
||||
):
|
||||
"""Test sensor with scaled data is not created if state is None."""
|
||||
data = {
|
||||
"sensors": {
|
||||
"1": {
|
||||
"name": "Sensor 1",
|
||||
"type": sensor_type,
|
||||
"state": {sensor_property: None},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:00-00",
|
||||
}
|
||||
}
|
||||
}
|
||||
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||
await setup_deconz_integration(hass, aioclient_mock)
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket):
|
||||
"""Test that a sensor without an initial battery state creates a battery sensor once state exist."""
|
||||
data = {
|
||||
|
||||
@@ -75,6 +75,40 @@ async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> N
|
||||
# TODO: No states, should the cost entity refuse to setup?
|
||||
|
||||
|
||||
async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) -> None:
|
||||
"""Test sensor attributes."""
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
energy_data["energy_sources"].append(
|
||||
{
|
||||
"type": "grid",
|
||||
"flow_from": [
|
||||
{
|
||||
"stat_energy_from": "sensor.energy_consumption",
|
||||
"entity_energy_from": "sensor.energy_consumption",
|
||||
"stat_cost": None,
|
||||
"entity_energy_price": None,
|
||||
"number_energy_price": 1,
|
||||
}
|
||||
],
|
||||
"flow_to": [],
|
||||
"cost_adjustment_day": 0,
|
||||
}
|
||||
)
|
||||
|
||||
hass_storage[data.STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": energy_data,
|
||||
}
|
||||
await setup_integration(hass)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
cost_sensor_entity_id = "sensor.energy_consumption_cost"
|
||||
entry = registry.async_get(cost_sensor_entity_id)
|
||||
assert entry.entity_category is None
|
||||
assert entry.disabled_by is None
|
||||
assert entry.hidden_by == er.RegistryEntryHider.INTEGRATION
|
||||
|
||||
|
||||
@pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")])
|
||||
@pytest.mark.parametrize(
|
||||
"price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)]
|
||||
|
||||
@@ -165,6 +165,48 @@ async def test_form_only_still_sample(hass, user_flow, image_file):
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize(
|
||||
("template", "url", "expected_result"),
|
||||
[
|
||||
# Test we can handle templates in strange parts of the url, #70961.
|
||||
(
|
||||
"http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png",
|
||||
"http://localhost:8123/static/icons/favicon-apple-180x180.png",
|
||||
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
|
||||
),
|
||||
(
|
||||
"{% if 1 %}https://bla{% else %}https://yo{% endif %}",
|
||||
"https://bla/",
|
||||
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
|
||||
),
|
||||
(
|
||||
"http://{{example.org",
|
||||
"http://example.org",
|
||||
data_entry_flow.RESULT_TYPE_FORM,
|
||||
),
|
||||
(
|
||||
"invalid1://invalid:4\\1",
|
||||
"invalid1://invalid:4%5c1",
|
||||
data_entry_flow.RESULT_TYPE_CREATE_ENTRY,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_still_template(
|
||||
hass, user_flow, fakeimgbytes_png, template, url, expected_result
|
||||
) -> None:
|
||||
"""Test we can handle various templates."""
|
||||
respx.get(url).respond(stream=fakeimgbytes_png)
|
||||
data = TESTDATA.copy()
|
||||
data.pop(CONF_STREAM_SOURCE)
|
||||
data[CONF_STILL_IMAGE_URL] = template
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
user_flow["flow_id"],
|
||||
data,
|
||||
)
|
||||
assert result2["type"] == expected_result
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow):
|
||||
"""Test we complete ok if the user enters a stream url."""
|
||||
|
||||
@@ -147,20 +147,13 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header):
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
entity_entry3 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_system_id",
|
||||
suggested_object_id="system_switch",
|
||||
entity_category=EntityCategory.SYSTEM,
|
||||
)
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_hidden_integration_id",
|
||||
suggested_object_id="hidden_integration_switch",
|
||||
hidden_by=er.RegistryEntryHider.INTEGRATION,
|
||||
)
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
entity_entry4 = entity_registry.async_get_or_create(
|
||||
"switch",
|
||||
"test",
|
||||
"switch_hidden_user_id",
|
||||
@@ -173,7 +166,6 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header):
|
||||
hass_fixture.states.async_set(entity_entry2.entity_id, "something_else")
|
||||
hass_fixture.states.async_set(entity_entry3.entity_id, "blah")
|
||||
hass_fixture.states.async_set(entity_entry4.entity_id, "foo")
|
||||
hass_fixture.states.async_set(entity_entry5.entity_id, "bar")
|
||||
|
||||
reqid = "5711642932632160983"
|
||||
data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"program_lock_on": false,
|
||||
"blink_on_tx_on": false,
|
||||
"resume_dim_on": false,
|
||||
"led_on": false,
|
||||
"led_off": false,
|
||||
"key_beep_on": false,
|
||||
"rf_disable_on": false,
|
||||
"powerline_disable_on": false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Mock devices object to test Insteon."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pyinsteon.address import Address
|
||||
from pyinsteon.constants import ALDBStatus, ResponseStatus
|
||||
@@ -129,18 +129,20 @@ class MockDevices:
|
||||
|
||||
def fill_properties(self, address, props_dict):
|
||||
"""Fill the operating flags and extended properties of a device."""
|
||||
|
||||
device = self._devices[Address(address)]
|
||||
operating_flags = props_dict.get("operating_flags", {})
|
||||
properties = props_dict.get("properties", {})
|
||||
|
||||
for flag in operating_flags:
|
||||
value = operating_flags[flag]
|
||||
if device.operating_flags.get(flag):
|
||||
device.operating_flags[flag].load(value)
|
||||
for flag in properties:
|
||||
value = properties[flag]
|
||||
if device.properties.get(flag):
|
||||
device.properties[flag].load(value)
|
||||
with patch("pyinsteon.subscriber_base.publish_topic", MagicMock()):
|
||||
for flag in operating_flags:
|
||||
value = operating_flags[flag]
|
||||
if device.operating_flags.get(flag):
|
||||
device.operating_flags[flag].load(value)
|
||||
for flag in properties:
|
||||
value = properties[flag]
|
||||
if device.properties.get(flag):
|
||||
device.properties[flag].load(value)
|
||||
|
||||
async def async_add_device(self, address=None, multiple=False):
|
||||
"""Mock the async_add_device method."""
|
||||
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from kostal.plenticore import MeData, SettingsData, VersionData
|
||||
from kostal.plenticore import MeData, VersionData
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.kostal_plenticore.helper import Plenticore
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
@@ -14,10 +15,19 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> Generator[None, MockConfigEntry, None]:
|
||||
"""Set up Kostal Plenticore integration for testing."""
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mocked ConfigEntry for testing."""
|
||||
return MockConfigEntry(
|
||||
entry_id="2ab8dd92a62787ddfe213a67e09406bd",
|
||||
title="scb",
|
||||
domain="kostal_plenticore",
|
||||
data={"host": "192.168.1.2", "password": "SecretPassword"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_plenticore() -> Generator[Plenticore, None, None]:
|
||||
"""Set up a Plenticore mock with some default values."""
|
||||
with patch(
|
||||
"homeassistant.components.kostal_plenticore.Plenticore", autospec=True
|
||||
) as mock_api_class:
|
||||
@@ -60,37 +70,20 @@ async def init_integration(
|
||||
)
|
||||
|
||||
plenticore.client.get_process_data = AsyncMock()
|
||||
plenticore.client.get_process_data.return_value = {
|
||||
"devices:local": ["HomeGrid_P", "HomePv_P"]
|
||||
}
|
||||
|
||||
plenticore.client.get_settings = AsyncMock()
|
||||
plenticore.client.get_settings.return_value = {
|
||||
"devices:local": [
|
||||
SettingsData(
|
||||
{
|
||||
"id": "Battery:MinSoc",
|
||||
"unit": "%",
|
||||
"default": "None",
|
||||
"min": 5,
|
||||
"max": 100,
|
||||
"type": "byte",
|
||||
"access": "readwrite",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
mock_config_entry = MockConfigEntry(
|
||||
entry_id="2ab8dd92a62787ddfe213a67e09406bd",
|
||||
title="scb",
|
||||
domain="kostal_plenticore",
|
||||
data={"host": "192.168.1.2", "password": "SecretPassword"},
|
||||
)
|
||||
yield plenticore
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> MockConfigEntry:
|
||||
"""Set up Kostal Plenticore integration for testing."""
|
||||
|
||||
yield mock_config_entry
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Test Kostal Plenticore diagnostics."""
|
||||
from aiohttp import ClientSession
|
||||
from kostal.plenticore import SettingsData
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED
|
||||
from homeassistant.components.kostal_plenticore.helper import Plenticore
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -9,9 +11,34 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry
|
||||
):
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSession,
|
||||
mock_plenticore: Plenticore,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
|
||||
# set some test process and settings data for the diagnostics output
|
||||
mock_plenticore.client.get_process_data.return_value = {
|
||||
"devices:local": ["HomeGrid_P", "HomePv_P"]
|
||||
}
|
||||
|
||||
mock_plenticore.client.get_settings.return_value = {
|
||||
"devices:local": [
|
||||
SettingsData(
|
||||
{
|
||||
"id": "Battery:MinSoc",
|
||||
"unit": "%",
|
||||
"default": "None",
|
||||
"min": 5,
|
||||
"max": 100,
|
||||
"type": "byte",
|
||||
"access": "readwrite",
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, init_integration
|
||||
) == {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Test the Kostal Plenticore Solar Inverter select platform."""
|
||||
from kostal.plenticore import SettingsData
|
||||
|
||||
from homeassistant.components.kostal_plenticore.helper import Plenticore
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_select_battery_charging_usage_available(
|
||||
hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that the battery charging usage select entity is added if the settings are available."""
|
||||
|
||||
mock_plenticore.client.get_settings.return_value = {
|
||||
"devices:local": [
|
||||
SettingsData({"id": "Battery:SmartBatteryControl:Enable"}),
|
||||
SettingsData({"id": "Battery:TimeControl:Enable"}),
|
||||
]
|
||||
}
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity_registry.async_get(hass).async_is_registered(
|
||||
"select.battery_charging_usage_mode"
|
||||
)
|
||||
|
||||
|
||||
async def test_select_battery_charging_usage_not_available(
|
||||
hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that the battery charging usage select entity is not added if the settings are unavailable."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not entity_registry.async_get(hass).async_is_registered(
|
||||
"select.battery_charging_usage_mode"
|
||||
)
|
||||
@@ -73,6 +73,18 @@ async def test_process_play_media_url(hass, mock_sign_path):
|
||||
== "http://192.168.123.123:8123/path?hello=world"
|
||||
)
|
||||
|
||||
# Test skip signing URLs if they are known to require no auth
|
||||
assert (
|
||||
async_process_play_media_url(hass, "/api/tts_proxy/bla")
|
||||
== "http://example.local:8123/api/tts_proxy/bla"
|
||||
)
|
||||
assert (
|
||||
async_process_play_media_url(
|
||||
hass, "http://example.local:8123/api/tts_proxy/bla"
|
||||
)
|
||||
== "http://example.local:8123/api/tts_proxy/bla"
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async_process_play_media_url(hass, "hello")
|
||||
|
||||
|
||||
@@ -488,7 +488,7 @@ async def test_media_commands(mocked_status, mocked_volume, hass, one_device):
|
||||
assert mocked_volume.call_count == 2
|
||||
|
||||
entity_1_state = hass.states.get("media_player.soundtouch_1")
|
||||
assert entity_1_state.attributes["supported_features"] == 20413
|
||||
assert entity_1_state.attributes["supported_features"] == 151485
|
||||
|
||||
|
||||
@patch("libsoundtouch.device.SoundTouchDevice.power_off")
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.steam_online import DOMAIN
|
||||
from homeassistant.components.steam_online.const import CONF_ACCOUNT, CONF_ACCOUNTS
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -19,38 +19,19 @@ CONF_DATA = {
|
||||
CONF_ACCOUNT: ACCOUNT_1,
|
||||
}
|
||||
|
||||
CONF_OPTIONS = {
|
||||
CONF_ACCOUNTS: {
|
||||
ACCOUNT_1: {
|
||||
CONF_NAME: ACCOUNT_NAME_1,
|
||||
"enabled": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
CONF_OPTIONS = {CONF_ACCOUNTS: {ACCOUNT_1: ACCOUNT_NAME_1}}
|
||||
|
||||
CONF_OPTIONS_2 = {
|
||||
CONF_ACCOUNTS: {
|
||||
ACCOUNT_1: {
|
||||
CONF_NAME: ACCOUNT_NAME_1,
|
||||
"enabled": True,
|
||||
},
|
||||
ACCOUNT_2: {
|
||||
CONF_NAME: ACCOUNT_NAME_2,
|
||||
"enabled": True,
|
||||
},
|
||||
ACCOUNT_1: ACCOUNT_NAME_1,
|
||||
ACCOUNT_2: ACCOUNT_NAME_2,
|
||||
}
|
||||
}
|
||||
|
||||
CONF_IMPORT_OPTIONS = {
|
||||
CONF_ACCOUNTS: {
|
||||
ACCOUNT_1: {
|
||||
CONF_NAME: ACCOUNT_NAME_1,
|
||||
"enabled": True,
|
||||
},
|
||||
ACCOUNT_2: {
|
||||
CONF_NAME: ACCOUNT_NAME_2,
|
||||
"enabled": True,
|
||||
},
|
||||
ACCOUNT_1: ACCOUNT_NAME_1,
|
||||
ACCOUNT_2: ACCOUNT_NAME_2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Test Steam config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import steam
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -25,7 +27,10 @@ from . import (
|
||||
|
||||
async def test_flow_user(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow."""
|
||||
with patch_interface():
|
||||
with patch_interface(), patch(
|
||||
"homeassistant.components.steam_online.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
|
||||
@@ -1025,6 +1025,7 @@ async def test_color_action_no_template(hass, start_ha, calls):
|
||||
((359.9, 99.9), {"replace6": '"{{(359.9, 99.9)}}"'}),
|
||||
(None, {"replace6": '"{{(361, 100)}}"'}),
|
||||
(None, {"replace6": '"{{(360, 101)}}"'}),
|
||||
(None, {"replace6": '"[{{(360)}},{{null}}]"'}),
|
||||
(None, {"replace6": '"{{x - 12}}"'}),
|
||||
(None, {"replace6": '""'}),
|
||||
(None, {"replace6": '"{{ none }}"'}),
|
||||
|
||||
@@ -542,6 +542,17 @@ async def test_simple_option_flow(hass, aioclient_mock):
|
||||
}
|
||||
|
||||
|
||||
async def test_option_flow_integration_not_setup(hass, aioclient_mock):
|
||||
"""Test advanced config flow options."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
|
||||
hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "integration_not_setup"
|
||||
|
||||
|
||||
async def test_form_ssdp(hass):
|
||||
"""Test we get the form with ssdp source."""
|
||||
|
||||
|
||||
@@ -913,7 +913,6 @@ async def test_entity_category_property(hass):
|
||||
(
|
||||
("config", entity.EntityCategory.CONFIG),
|
||||
("diagnostic", entity.EntityCategory.DIAGNOSTIC),
|
||||
("system", entity.EntityCategory.SYSTEM),
|
||||
),
|
||||
)
|
||||
def test_entity_category_schema(value, expected):
|
||||
@@ -930,7 +929,7 @@ def test_entity_category_schema_error(value):
|
||||
schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA)
|
||||
with pytest.raises(
|
||||
vol.Invalid,
|
||||
match=r"expected EntityCategory or one of 'config', 'diagnostic', 'system'",
|
||||
match=r"expected EntityCategory or one of 'config', 'diagnostic'",
|
||||
):
|
||||
schema(value)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def assert_element(trace_element, expected_element, path):
|
||||
# Check for unexpected items in trace_element
|
||||
assert not set(trace_element._result or {}) - set(expected_result)
|
||||
|
||||
if "error_type" in expected_element:
|
||||
if "error_type" in expected_element and expected_element["error_type"] is not None:
|
||||
assert isinstance(trace_element._error, expected_element["error_type"])
|
||||
else:
|
||||
assert trace_element._error is None
|
||||
@@ -4485,6 +4485,65 @@ async def test_stop_action(hass, caplog):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error,error_type,logmsg,script_execution",
|
||||
(
|
||||
(True, script._AbortScript, "Error", "aborted"),
|
||||
(False, None, "Stop", "finished"),
|
||||
),
|
||||
)
|
||||
async def test_stop_action_subscript(
|
||||
hass, caplog, error, error_type, logmsg, script_execution
|
||||
):
|
||||
"""Test if automation stops on calling the stop action from a sub-script."""
|
||||
event = "test_event"
|
||||
events = async_capture_events(hass, event)
|
||||
|
||||
alias = "stop step"
|
||||
sequence = cv.SCRIPT_SCHEMA(
|
||||
[
|
||||
{"event": event},
|
||||
{
|
||||
"if": {
|
||||
"alias": "if condition",
|
||||
"condition": "template",
|
||||
"value_template": "{{ 1 == 1 }}",
|
||||
},
|
||||
"then": {
|
||||
"alias": alias,
|
||||
"stop": "In the name of love",
|
||||
"error": error,
|
||||
},
|
||||
},
|
||||
{"event": event},
|
||||
]
|
||||
)
|
||||
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
|
||||
|
||||
await script_obj.async_run(context=Context())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert f"{logmsg} script sequence: In the name of love" in caplog.text
|
||||
caplog.clear()
|
||||
assert len(events) == 1
|
||||
|
||||
assert_action_trace(
|
||||
{
|
||||
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||
"1": [{"error_type": error_type, "result": {"choice": "then"}}],
|
||||
"1/if": [{"result": {"result": True}}],
|
||||
"1/if/condition/0": [{"result": {"result": True, "entities": []}}],
|
||||
"1/then/0": [
|
||||
{
|
||||
"error_type": error_type,
|
||||
"result": {"stop": "In the name of love", "error": error},
|
||||
}
|
||||
],
|
||||
},
|
||||
expected_script_execution=script_execution,
|
||||
)
|
||||
|
||||
|
||||
async def test_stop_action_with_error(hass, caplog):
|
||||
"""Test if automation fails on calling the error action."""
|
||||
event = "test_event"
|
||||
|
||||
@@ -27,7 +27,7 @@ async def test_at_start_when_running_awaitable(hass):
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
async def test_at_start_when_running_callback(hass):
|
||||
async def test_at_start_when_running_callback(hass, caplog):
|
||||
"""Test at start when already running."""
|
||||
assert hass.state == core.CoreState.running
|
||||
assert hass.is_running
|
||||
@@ -39,15 +39,19 @@ async def test_at_start_when_running_callback(hass):
|
||||
"""Home Assistant is started."""
|
||||
calls.append(1)
|
||||
|
||||
start.async_at_start(hass, cb_at_start)
|
||||
start.async_at_start(hass, cb_at_start)()
|
||||
assert len(calls) == 1
|
||||
|
||||
hass.state = core.CoreState.starting
|
||||
assert hass.is_running
|
||||
|
||||
start.async_at_start(hass, cb_at_start)
|
||||
start.async_at_start(hass, cb_at_start)()
|
||||
assert len(calls) == 2
|
||||
|
||||
# Check the unnecessary cancel did not generate warnings or errors
|
||||
for record in caplog.records:
|
||||
assert record.levelname in ("DEBUG", "INFO")
|
||||
|
||||
|
||||
async def test_at_start_when_starting_awaitable(hass):
|
||||
"""Test at start when yet to start."""
|
||||
@@ -69,7 +73,7 @@ async def test_at_start_when_starting_awaitable(hass):
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
async def test_at_start_when_starting_callback(hass):
|
||||
async def test_at_start_when_starting_callback(hass, caplog):
|
||||
"""Test at start when yet to start."""
|
||||
hass.state = core.CoreState.not_running
|
||||
assert not hass.is_running
|
||||
@@ -81,10 +85,57 @@ async def test_at_start_when_starting_callback(hass):
|
||||
"""Home Assistant is started."""
|
||||
calls.append(1)
|
||||
|
||||
start.async_at_start(hass, cb_at_start)
|
||||
cancel = start.async_at_start(hass, cb_at_start)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
cancel()
|
||||
|
||||
# Check the unnecessary cancel did not generate warnings or errors
|
||||
for record in caplog.records:
|
||||
assert record.levelname in ("DEBUG", "INFO")
|
||||
|
||||
|
||||
async def test_cancelling_when_running(hass, caplog):
|
||||
"""Test cancelling at start when already running."""
|
||||
assert hass.state == core.CoreState.running
|
||||
assert hass.is_running
|
||||
|
||||
calls = []
|
||||
|
||||
async def cb_at_start(hass):
|
||||
"""Home Assistant is started."""
|
||||
calls.append(1)
|
||||
|
||||
start.async_at_start(hass, cb_at_start)()
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
# Check the unnecessary cancel did not generate warnings or errors
|
||||
for record in caplog.records:
|
||||
assert record.levelname in ("DEBUG", "INFO")
|
||||
|
||||
|
||||
async def test_cancelling_when_starting(hass):
|
||||
"""Test cancelling at start when yet to start."""
|
||||
hass.state = core.CoreState.not_running
|
||||
assert not hass.is_running
|
||||
|
||||
calls = []
|
||||
|
||||
@core.callback
|
||||
def cb_at_start(hass):
|
||||
"""Home Assistant is started."""
|
||||
calls.append(1)
|
||||
|
||||
start.async_at_start(hass, cb_at_start)()
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
Reference in New Issue
Block a user