Compare commits

...

44 Commits

Author SHA1 Message Date
Paulus Schoutsen 8ac5dd32a8 Bumped version to 2022.5.0b6 2022-05-02 15:40:51 -07:00
Paulus Schoutsen 06f939b0ac Bump frontend to 20220502.0 (#71221) 2022-05-02 15:40:22 -07:00
Paulus Schoutsen d213cc3c8e Add media source support to Bose Soundtouch (#71209) 2022-05-02 15:40:21 -07:00
Paulus Schoutsen 6cac24887c Skip signing URL that we know requires no auth (#71208) 2022-05-02 15:40:20 -07:00
Franck Nijhof 71506bd02b Adjust version number in template default deprecation warning (#71203) 2022-05-02 15:40:20 -07:00
David F. Mulcahey 2b87e03fac Fix bad ZHA _attr definitions (#71198) 2022-05-02 15:40:19 -07:00
Erik Montnemery 4dfcc9dcf9 Allow cancelling async_at_start helper (#71196) 2022-05-02 15:40:18 -07:00
Erik Montnemery fed4d0a38d Stop script if sub-script stops or aborts (#71195) 2022-05-02 15:40:18 -07:00
Erik Montnemery c37fec67a1 Remove entity registry entries when script is removed (#71193) 2022-05-02 15:40:17 -07:00
Robert Svensson 35bc812397 Make sure sensor state value is not None prior to trying to used the scaled value (#71189) 2022-05-02 15:40:16 -07:00
epenet d97bce2d9d Fix Renault diagnostics (#71186) 2022-05-02 15:40:15 -07:00
Paulus Schoutsen c07e36283b Add media source support to AppleTV (#71185) 2022-05-02 15:40:15 -07:00
Paulus Schoutsen b558425333 Offer visit device for Squeezelite32 devices (#71181) 2022-05-02 15:40:14 -07:00
Tom Harris 6bb3957730 Fix Insteon thermostats and reduce logging (#71179)
* Bump pyinsteon to 1.1.0

* Load modem aldb if read write mode is unkwown

* Correct reference to read_write_mode
2022-05-02 15:40:13 -07:00
Zoltán Tóth b8e0066ec2 Fix SAJ Solar inverter RecursionError (#71157) 2022-05-02 15:40:13 -07:00
Robert Svensson 72eb963c7a Handle situation where mac might not exist in clients (#71016) 2022-05-02 15:40:12 -07:00
Erik Montnemery 494902e185 Remove entity category system in favor of hidden_by (#68550) 2022-05-02 15:40:11 -07:00
stegm 0ec29711e1 Handle missing kostal plenticore battery option (#65237)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-05-02 15:40:10 -07:00
Paulus Schoutsen 78439eebb9 Bumped version to 2022.5.0b5 2022-05-01 21:53:15 -07:00
Aaron Bach d4780ac43c Fix issues with SimpliSafe email-based 2FA (#71180)
* FIx issues with email-based SimpliSafe 2FA

* Bump
2022-05-01 21:53:11 -07:00
Allen Porter 5c4861011b Bump gcal_sync to 0.7.1 to fix calendar API timezone handling (#71173) 2022-05-01 21:53:11 -07:00
G Johansson c791c52d28 Fix template error in sql (#71169) 2022-05-01 21:53:10 -07:00
Matt Zimmerman 0d4947a2d3 update python-smarttub to 0.0.32 (#71164) 2022-05-01 21:53:09 -07:00
J. Nick Koston 1f912e9c98 Bump zeroconf to 0.38.5 (#71160) 2022-05-01 21:53:08 -07:00
J. Nick Koston db53b3cbe0 Fix missing device info in lutron_caseta (#71156)
- There was a missing return due to a bad merge conflict resolution

- Fixes #71154
2022-05-01 21:53:08 -07:00
G Johansson e7bcf839ac Bump pysensibo 1.0.14 (#71150) 2022-05-01 21:53:07 -07:00
Marvin Wichmann 79cc216327 Update xknx to 0.21.1 (#71144) 2022-05-01 21:53:06 -07:00
Kuba Wolanin 01b096bb09 Add Show logs URL to integration errors notification (#71142) 2022-05-01 21:53:06 -07:00
Robert Svensson 174717dd85 Abort UniFi Network options flow if integration is not setup (#71128) 2022-05-01 21:53:05 -07:00
Robert Svensson 4751356638 Make deCONZ SSDP discovery more strict by matching on manufacturerURL (#71124) 2022-05-01 21:53:04 -07:00
Robert Svensson 47d19b3967 Fix copy paste issue leaving one device trigger with a wrong subtype (#71121) 2022-05-01 21:53:04 -07:00
Allen Porter c1bbcfd275 Bump gcal_sync to 0.7.0 (#71116) 2022-05-01 21:53:03 -07:00
Matthias Alphart adc2f3d169 Update xknx to 0.21.0 (#71108) 2022-05-01 21:53:02 -07:00
Shay Levy 6a110e5a77 Add entity id to template error logging (#71107)
* Add entity id to template error logging

* Increase coverage
2022-05-01 21:53:02 -07:00
G Johansson 3ce531e2f1 Sensibo bugfix device on (#71106)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-05-01 21:53:01 -07:00
G Johansson ce73b517b8 Bump pysensibo to 1.0.13 (#71105) 2022-05-01 21:53:00 -07:00
Franck Nijhof 4f784c42ab Fix missing device & entity references in automations (#71103) 2022-05-01 21:52:59 -07:00
Raman Gupta c0b6d6a44e Bump zwave-js-server-python to 0.36.1 (#71096) 2022-05-01 21:52:59 -07:00
Tom Harris 4346d8cc2f Fix Insteon tests (#71092) 2022-05-01 21:52:58 -07:00
Robert Hillis 4c7c7b72b7 Clean up Steam integration (#71091)
* Clean up Steam

* uno mas

* uno mas

* uno mas
2022-05-01 21:52:57 -07:00
J. Nick Koston 26b6952c06 Reduce calls to asyncio.iscoroutine (#71090) 2022-05-01 21:52:56 -07:00
Dave T 5d37cfc61e Generic camera handle template adjacent to portnumber (#71031) 2022-05-01 21:52:56 -07:00
Michael 51aa070e19 Fix "station is open" binary sensor in Tankerkoenig (#70928) 2022-05-01 21:52:55 -07:00
Diogo Gomes 205a8fc752 update unit_of_measurement even if unit_of_measurement is known (#69699) 2022-05-01 21:52:54 -07:00
88 changed files with 840 additions and 405 deletions
@@ -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)
+1 -1
View File
@@ -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)
+14 -2
View File
@@ -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"],
+4 -4
View File
@@ -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",
+20 -10
View File
@@ -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"]
+3 -1
View File
@@ -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 (
+1 -1
View File
@@ -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()
},
}
+3 -6
View File
@@ -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
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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:
+22 -8
View File
@@ -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
+21 -9
View File
@@ -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
+9 -4
View File
@@ -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
+4 -1
View File
@@ -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",
+4 -4
View File
@@ -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",
+2 -1
View File
@@ -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)."
+1 -3
View File
@@ -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
+4 -5
View File
@@ -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
+2 -1
View File
@@ -24,7 +24,8 @@ SSDP = {
],
"deconz": [
{
"manufacturer": "Royal Philips Electronics"
"manufacturer": "Royal Philips Electronics",
"manufacturerURL": "http://www.dresden-elektronik.de"
}
],
"denonavr": [
-3
View File
@@ -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)
+1 -1
View File
@@ -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,
+8 -2
View File
@@ -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(
+13 -2
View File
@@ -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
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
+9 -9
View File
@@ -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
+9 -9
View File
@@ -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 -1
View File
@@ -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
+1 -9
View File
@@ -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)
+51 -9
View File
@@ -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",
}
+1 -11
View File
@@ -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(
+3 -14
View File
@@ -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)
+17 -2
View File
@@ -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)
+30
View File
@@ -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 = {
+34
View File
@@ -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,
+11 -9
View File
@@ -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."""
+27 -34
View File
@@ -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")
+6 -25
View File
@@ -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},
+1
View File
@@ -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."""
+1 -2
View File
@@ -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)
+60 -1
View File
@@ -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"
+56 -5
View File
@@ -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