This commit is contained in:
Bram Kragten 2023-09-08 22:46:00 +02:00 committed by GitHub
commit 9e6ac1d7ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 732 additions and 202 deletions

View File

@ -416,7 +416,6 @@ omit =
homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/home_base.py
homeassistant/components/freebox/router.py
homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py
homeassistant/components/fritz/common.py
homeassistant/components/fritz/device_tracker.py

View File

@ -90,6 +90,13 @@ class AlexaUnsupportedThermostatModeError(AlexaError):
error_type = "UNSUPPORTED_THERMOSTAT_MODE"
class AlexaUnsupportedThermostatTargetStateError(AlexaError):
"""Class to represent unsupported climate target state error."""
namespace = "Alexa.ThermostatController"
error_type = "INVALID_TARGET_STATE"
class AlexaTempRangeError(AlexaError):
"""Class to represent TempRange errors."""

View File

@ -73,6 +73,7 @@ from .errors import (
AlexaSecurityPanelAuthorizationRequired,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
AlexaUnsupportedThermostatTargetStateError,
AlexaVideoActionNotPermittedForContentError,
)
from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode
@ -911,7 +912,13 @@ async def async_api_adjust_target_temp(
}
)
else:
target_temp = float(entity.attributes[ATTR_TEMPERATURE]) + temp_delta
current_target_temp: str | None = entity.attributes.get(ATTR_TEMPERATURE)
if current_target_temp is None:
raise AlexaUnsupportedThermostatTargetStateError(
"The current target temperature is not set, "
"cannot adjust target temperature"
)
target_temp = float(current_target_temp) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)

View File

@ -28,7 +28,13 @@ from homeassistant.const import (
)
import homeassistant.core as ha
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
from homeassistant.exceptions import (
InvalidEntityFormatError,
InvalidStateError,
ServiceNotFound,
TemplateError,
Unauthorized,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.service import async_get_all_descriptions
@ -222,7 +228,7 @@ class APIEntityStateView(HomeAssistantView):
"""Update state of entity."""
if not request["hass_user"].is_admin:
raise Unauthorized(entity_id=entity_id)
hass = request.app["hass"]
hass: HomeAssistant = request.app["hass"]
try:
data = await request.json()
except ValueError:
@ -237,9 +243,16 @@ class APIEntityStateView(HomeAssistantView):
is_new_state = hass.states.get(entity_id) is None
# Write state
try:
hass.states.async_set(
entity_id, new_state, attributes, force_update, self.context(request)
)
except InvalidEntityFormatError:
return self.json_message(
"Invalid entity ID specified.", HTTPStatus.BAD_REQUEST
)
except InvalidStateError:
return self.json_message("Invalid state specified.", HTTPStatus.BAD_REQUEST)
# Read the state back for our response
status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK

View File

@ -15,10 +15,10 @@
"quality_scale": "internal",
"requirements": [
"bleak==0.21.0",
"bleak-retry-connector==3.1.2",
"bluetooth-adapters==0.16.0",
"bluetooth-auto-recovery==1.2.1",
"bleak-retry-connector==3.1.3",
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.2",
"bluetooth-data-tools==1.11.0",
"dbus-fast==1.94.1"
"dbus-fast==1.95.2"
]
}

View File

@ -518,6 +518,8 @@ class ElkEntity(Entity):
def device_info(self) -> DeviceInfo:
"""Device info connecting via the ElkM1 system."""
return DeviceInfo(
name=self._element.name,
identifiers={(DOMAIN, self._unique_id)},
via_device=(DOMAIN, f"{self._prefix}_system"),
)

View File

@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense_energy==0.12.0"]
"requirements": ["sense_energy==0.12.1"]
}

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.9.1"],
"requirements": ["pyenphase==1.11.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/epson",
"iot_class": "local_polling",
"loggers": ["epson_projector"],
"requirements": ["epson-projector==0.5.0"]
"requirements": ["epson-projector==0.5.1"]
}

View File

@ -6,7 +6,7 @@ import logging
from epson_projector import Projector, ProjectorUnavailableError
from epson_projector.const import (
BACK,
BUSY,
BUSY_CODES,
CMODE,
CMODE_LIST,
CMODE_LIST_SET,
@ -147,7 +147,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity):
self._attr_volume_level = float(volume)
except ValueError:
self._attr_volume_level = None
elif power_state == BUSY:
elif power_state in BUSY_CODES:
self._attr_state = MediaPlayerState.ON
else:
self._attr_state = MediaPlayerState.OFF

View File

@ -85,4 +85,7 @@ CATEGORY_TO_MODEL = {
HOME_COMPATIBLE_CATEGORIES = [
FreeboxHomeCategory.CAMERA,
FreeboxHomeCategory.DWS,
FreeboxHomeCategory.KFB,
FreeboxHomeCategory.PIR,
]

View File

@ -156,7 +156,12 @@ class FreeboxRouter:
fbx_disks: list[dict[str, Any]] = await self._api.storage.get_disks() or []
for fbx_disk in fbx_disks:
self.disks[fbx_disk["id"]] = fbx_disk
disk: dict[str, Any] = {**fbx_disk}
disk_part: dict[int, dict[str, Any]] = {}
for fbx_disk_part in fbx_disk["partitions"]:
disk_part[fbx_disk_part["id"]] = fbx_disk_part
disk["partitions"] = disk_part
self.disks[fbx_disk["id"]] = disk
async def _update_raids_sensors(self) -> None:
"""Update Freebox raids."""

View File

@ -95,7 +95,7 @@ async def async_setup_entry(
entities.extend(
FreeboxDiskSensor(router, disk, partition, description)
for disk in router.disks.values()
for partition in disk["partitions"]
for partition in disk["partitions"].values()
for description in DISK_PARTITION_SENSORS
)
@ -197,7 +197,8 @@ class FreeboxDiskSensor(FreeboxSensor):
) -> None:
"""Initialize a Freebox disk sensor."""
super().__init__(router, description)
self._partition = partition
self._disk_id = disk["id"]
self._partition_id = partition["id"]
self._attr_name = f"{partition['label']} {description.name}"
self._attr_unique_id = (
f"{router.mac} {description.key} {disk['id']} {partition['id']}"
@ -218,10 +219,10 @@ class FreeboxDiskSensor(FreeboxSensor):
def async_update_state(self) -> None:
"""Update the Freebox disk sensor."""
value = None
if self._partition.get("total_bytes"):
value = round(
self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2
)
disk: dict[str, Any] = self._router.disks[self._disk_id]
partition: dict[str, Any] = disk["partitions"][self._partition_id]
if partition.get("total_bytes"):
value = round(partition["free_bytes"] * 100 / partition["total_bytes"], 2)
self._attr_native_value = value

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230906.1"]
"requirements": ["home-assistant-frontend==20230908.0"]
}

View File

@ -90,7 +90,7 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""Get the latest data and updates the state."""
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
if self.entity_description.key == "status":
self._attr_is_on = self.coordinator.api.status == "All good!"
self._attr_is_on = self.coordinator.last_update_success
elif self.entity_description.key == "is_watering":
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
self._attr_is_on = relay_data["timestr"] == "Now"

View File

@ -33,7 +33,7 @@ async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> boo
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=coordinator.serial_number,
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Livisi",
name=f"SHC {coordinator.controller_type} {coordinator.serial_number}",

View File

@ -9,7 +9,7 @@
},
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.18.1"],
"requirements": ["pylutron-caseta==0.18.2"],
"zeroconf": [
{
"type": "_lutron._tcp.local.",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.11.1", "mill-local==0.2.0"]
"requirements": ["millheater==0.11.2", "mill-local==0.2.0"]
}

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "gold",
"requirements": ["pymodbus==3.4.1"]
"requirements": ["pymodbus==3.5.1"]
}

View File

@ -215,7 +215,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
"Empty template output for entity: %s with state topic: %s."
" Payload: '%s', with value template '%s'"
),
self._config[CONF_NAME],
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
self._config.get(CONF_VALUE_TEMPLATE),
@ -240,7 +240,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
"No matching payload found for entity: %s with state topic: %s."
" Payload: '%s'%s"
),
self._config[CONF_NAME],
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
template_info,

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any, Literal, TypedDict
import noaa_coops as coops
import requests
@ -17,6 +18,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.unit_system import METRIC_SYSTEM
if TYPE_CHECKING:
from pandas import Timestamp
_LOGGER = logging.getLogger(__name__)
CONF_STATION_ID = "station_id"
@ -76,40 +80,56 @@ def setup_platform(
add_entities([noaa_sensor], True)
class NOAATidesData(TypedDict):
"""Representation of a single tide."""
time_stamp: list[Timestamp]
hi_lo: list[Literal["L"] | Literal["H"]]
predicted_wl: list[float]
class NOAATidesAndCurrentsSensor(SensorEntity):
"""Representation of a NOAA Tides and Currents sensor."""
_attr_attribution = "Data provided by NOAA"
def __init__(self, name, station_id, timezone, unit_system, station):
def __init__(self, name, station_id, timezone, unit_system, station) -> None:
"""Initialize the sensor."""
self._name = name
self._station_id = station_id
self._timezone = timezone
self._unit_system = unit_system
self._station = station
self.data = None
self.data: NOAATidesData | None = None
@property
def name(self):
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of this device."""
attr = {}
attr: dict[str, Any] = {}
if self.data is None:
return attr
if self.data["hi_lo"][1] == "H":
attr["high_tide_time"] = self.data.index[1].strftime("%Y-%m-%dT%H:%M")
attr["high_tide_time"] = self.data["time_stamp"][1].strftime(
"%Y-%m-%dT%H:%M"
)
attr["high_tide_height"] = self.data["predicted_wl"][1]
attr["low_tide_time"] = self.data.index[2].strftime("%Y-%m-%dT%H:%M")
attr["low_tide_time"] = self.data["time_stamp"][2].strftime(
"%Y-%m-%dT%H:%M"
)
attr["low_tide_height"] = self.data["predicted_wl"][2]
elif self.data["hi_lo"][1] == "L":
attr["low_tide_time"] = self.data.index[1].strftime("%Y-%m-%dT%H:%M")
attr["low_tide_time"] = self.data["time_stamp"][1].strftime(
"%Y-%m-%dT%H:%M"
)
attr["low_tide_height"] = self.data["predicted_wl"][1]
attr["high_tide_time"] = self.data.index[2].strftime("%Y-%m-%dT%H:%M")
attr["high_tide_time"] = self.data["time_stamp"][2].strftime(
"%Y-%m-%dT%H:%M"
)
attr["high_tide_height"] = self.data["predicted_wl"][2]
return attr
@ -118,7 +138,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity):
"""Return the state of the device."""
if self.data is None:
return None
api_time = self.data.index[0]
api_time = self.data["time_stamp"][0]
if self.data["hi_lo"][0] == "H":
tidetime = api_time.strftime("%-I:%M %p")
return f"High tide at {tidetime}"
@ -142,8 +162,13 @@ class NOAATidesAndCurrentsSensor(SensorEntity):
units=self._unit_system,
time_zone=self._timezone,
)
self.data = df_predictions.head()
_LOGGER.debug("Data = %s", self.data)
api_data = df_predictions.head()
self.data = NOAATidesData(
time_stamp=list(api_data.index),
hi_lo=list(api_data["hi_lo"].values),
predicted_wl=list(api_data["predicted_wl"].values),
)
_LOGGER.debug("Data = %s", api_data)
_LOGGER.debug(
"Recent Tide data queried with start time set to %s",
begin.strftime("%m-%d-%Y %H:%M"),

View File

@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.10.1"],
"requirements": ["pyoverkiz==1.9.0"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",

View File

@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aiorecollect"],
"requirements": ["aiorecollect==1.0.8"]
"requirements": ["aiorecollect==2023.09.0"]
}

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.12.0"]
"requirements": ["sense-energy==0.12.1"]
}

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["hatasmota"],
"mqtt": ["tasmota/discovery/#"],
"requirements": ["HATasmota==0.7.0"]
"requirements": ["HATasmota==0.7.1"]
}

View File

@ -302,6 +302,8 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
[
TMRW_ATTR_TEMPERATURE_LOW,
TMRW_ATTR_TEMPERATURE_HIGH,
TMRW_ATTR_DEW_POINT,
TMRW_ATTR_HUMIDITY,
TMRW_ATTR_WIND_SPEED,
TMRW_ATTR_WIND_DIRECTION,
TMRW_ATTR_CONDITION,

View File

@ -122,7 +122,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if same_hub_entries:
await asyncio.wait(
[
self.hass.config_entries.async_remove(entry_id)
asyncio.create_task(self.hass.config_entries.async_remove(entry_id))
for entry_id in same_hub_entries
]
)

View File

@ -112,9 +112,9 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
dev_info, utc_point_in_time
),
)
for dev_info in (await self.api.get_all_devices()).values()
for dev_info in (await self.api.get_devices_data()).values()
}
data_sensors = await self.api.get_user_data()
data_sensors = await self.api.get_sensor_data()
await self.api.logout()
return UpdateCoordinatorDataType(data_devices, data_sensors)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/vodafone_station",
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"requirements": ["aiovodafone==0.0.6"]
"requirements": ["aiovodafone==0.1.0"]
}

View File

@ -542,9 +542,8 @@ async def handle_render_template(
timed_out = await template_obj.async_render_will_timeout(
timeout, variables, strict=msg["strict"], log_fn=log_fn
)
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
except TemplateError:
timed_out = False
if timed_out:
connection.send_error(
@ -565,7 +564,9 @@ async def handle_render_template(
if not report_errors:
return
connection.send_message(
messages.event_message(msg["id"], {"error": str(result)})
messages.event_message(
msg["id"], {"error": str(result), "level": "ERROR"}
)
)
return
@ -581,7 +582,6 @@ async def handle_render_template(
hass,
[TrackTemplate(template_obj, variables)],
_template_listener,
raise_on_template_error=True,
strict=msg["strict"],
log_fn=log_fn,
)

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.91.1"]
"requirements": ["zeroconf==0.98.0"]
}

View File

@ -21,7 +21,7 @@
"universal_silabs_flasher"
],
"requirements": [
"bellows==0.36.2",
"bellows==0.36.3",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.103",

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -917,7 +917,6 @@ class TrackTemplateResultInfo:
def async_setup(
self,
raise_on_template_error: bool,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
) -> None:
@ -955,8 +954,6 @@ class TrackTemplateResultInfo:
)
if info.exception:
if raise_on_template_error:
raise info.exception
if not log_fn:
_LOGGER.error(
"Error while processing template: %s",
@ -1239,7 +1236,6 @@ def async_track_template_result(
hass: HomeAssistant,
track_templates: Sequence[TrackTemplate],
action: TrackTemplateResultListener,
raise_on_template_error: bool = False,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
has_super_template: bool = False,
@ -1266,11 +1262,6 @@ def async_track_template_result(
An iterable of TrackTemplate.
action
Callable to call with results.
raise_on_template_error
When set to True, if there is an exception
processing the template during setup, the system
will raise the exception instead of setting up
tracking.
strict
When set to True, raise on undefined variables.
log_fn
@ -1286,7 +1277,7 @@ def async_track_template_result(
"""
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn)
tracker.async_setup(strict=strict, log_fn=log_fn)
return tracker

View File

@ -8,21 +8,21 @@ atomicwrites-homeassistant==1.4.1
attrs==23.1.0
awesomeversion==22.9.0
bcrypt==4.0.1
bleak-retry-connector==3.1.2
bleak-retry-connector==3.1.3
bleak==0.21.0
bluetooth-adapters==0.16.0
bluetooth-auto-recovery==1.2.1
bluetooth-adapters==0.16.1
bluetooth-auto-recovery==1.2.2
bluetooth-data-tools==1.11.0
certifi>=2021.5.30
ciso8601==2.3.0
cryptography==41.0.3
dbus-fast==1.94.1
dbus-fast==1.95.2
fnv-hash-fast==0.4.1
ha-av==10.1.1
hass-nabucasa==0.70.0
hassil==1.2.5
home-assistant-bluetooth==1.10.3
home-assistant-frontend==20230906.1
home-assistant-frontend==20230908.0
home-assistant-intents==2023.8.2
httpx==0.24.1
ifaddr==0.2.0
@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtcvad==2.0.10
yarl==1.9.2
zeroconf==0.91.1
zeroconf==0.98.0
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.9.0"
version = "2023.9.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -29,7 +29,7 @@ DoorBirdPy==2.1.0
HAP-python==4.7.1
# homeassistant.components.tasmota
HATasmota==0.7.0
HATasmota==0.7.1
# homeassistant.components.mastodon
Mastodon.py==1.5.1
@ -327,7 +327,7 @@ aiopyarr==23.4.0
aioqsw==0.3.4
# homeassistant.components.recollect_waste
aiorecollect==1.0.8
aiorecollect==2023.09.0
# homeassistant.components.ridwell
aioridwell==2023.07.0
@ -369,7 +369,7 @@ aiounifi==61
aiovlc==0.1.0
# homeassistant.components.vodafone_station
aiovodafone==0.0.6
aiovodafone==0.1.0
# homeassistant.components.waqi
aiowaqi==0.2.1
@ -509,7 +509,7 @@ beautifulsoup4==4.12.2
# beewi-smartclim==0.0.10
# homeassistant.components.zha
bellows==0.36.2
bellows==0.36.3
# homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0
@ -518,7 +518,7 @@ bimmer-connected==0.14.0
bizkaibus==0.1.1
# homeassistant.components.bluetooth
bleak-retry-connector==3.1.2
bleak-retry-connector==3.1.3
# homeassistant.components.bluetooth
bleak==0.21.0
@ -540,10 +540,10 @@ bluemaestro-ble==0.2.3
# bluepy==1.3.0
# homeassistant.components.bluetooth
bluetooth-adapters==0.16.0
bluetooth-adapters==0.16.1
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.2.1
bluetooth-auto-recovery==1.2.2
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@ -641,7 +641,7 @@ datadog==0.15.0
datapoint==0.9.8
# homeassistant.components.bluetooth
dbus-fast==1.94.1
dbus-fast==1.95.2
# homeassistant.components.debugpy
debugpy==1.6.7
@ -751,7 +751,7 @@ env-canada==0.5.36
ephem==4.1.2
# homeassistant.components.epson
epson-projector==0.5.0
epson-projector==0.5.1
# homeassistant.components.epsonworkforce
epsonprinter==0.0.9
@ -994,7 +994,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20230906.1
home-assistant-frontend==20230908.0
# homeassistant.components.conversation
home-assistant-intents==2023.8.2
@ -1213,7 +1213,7 @@ micloud==0.5
mill-local==0.2.0
# homeassistant.components.mill
millheater==0.11.1
millheater==0.11.2
# homeassistant.components.minio
minio==7.1.12
@ -1671,7 +1671,7 @@ pyedimax==0.2.1
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==1.9.1
pyenphase==1.11.0
# homeassistant.components.envisalink
pyenvisalink==4.6
@ -1821,7 +1821,7 @@ pylitejet==0.5.0
pylitterbot==2023.4.5
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1
pylutron-caseta==0.18.2
# homeassistant.components.lutron
pylutron==0.2.8
@ -1851,7 +1851,7 @@ pymitv==1.4.3
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.4.1
pymodbus==3.5.1
# homeassistant.components.monoprice
pymonoprice==0.4
@ -1916,7 +1916,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.10.1
pyoverkiz==1.9.0
# homeassistant.components.openweathermap
pyowm==3.2.0
@ -2369,10 +2369,10 @@ securetar==2023.3.0
sendgrid==6.8.2
# homeassistant.components.sense
sense-energy==0.12.0
sense-energy==0.12.1
# homeassistant.components.emulated_kasa
sense_energy==0.12.0
sense_energy==0.12.1
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.0
@ -2766,7 +2766,7 @@ zamg==0.3.0
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.91.1
zeroconf==0.98.0
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -28,7 +28,7 @@ DoorBirdPy==2.1.0
HAP-python==4.7.1
# homeassistant.components.tasmota
HATasmota==0.7.0
HATasmota==0.7.1
# homeassistant.components.doods
# homeassistant.components.generic
@ -302,7 +302,7 @@ aiopyarr==23.4.0
aioqsw==0.3.4
# homeassistant.components.recollect_waste
aiorecollect==1.0.8
aiorecollect==2023.09.0
# homeassistant.components.ridwell
aioridwell==2023.07.0
@ -344,7 +344,7 @@ aiounifi==61
aiovlc==0.1.0
# homeassistant.components.vodafone_station
aiovodafone==0.0.6
aiovodafone==0.1.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -430,13 +430,13 @@ base36==0.1.1
beautifulsoup4==4.12.2
# homeassistant.components.zha
bellows==0.36.2
bellows==0.36.3
# homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0
# homeassistant.components.bluetooth
bleak-retry-connector==3.1.2
bleak-retry-connector==3.1.3
# homeassistant.components.bluetooth
bleak==0.21.0
@ -451,10 +451,10 @@ blinkpy==0.21.0
bluemaestro-ble==0.2.3
# homeassistant.components.bluetooth
bluetooth-adapters==0.16.0
bluetooth-adapters==0.16.1
# homeassistant.components.bluetooth
bluetooth-auto-recovery==1.2.1
bluetooth-auto-recovery==1.2.2
# homeassistant.components.bluetooth
# homeassistant.components.esphome
@ -521,7 +521,7 @@ datadog==0.15.0
datapoint==0.9.8
# homeassistant.components.bluetooth
dbus-fast==1.94.1
dbus-fast==1.95.2
# homeassistant.components.debugpy
debugpy==1.6.7
@ -604,7 +604,7 @@ env-canada==0.5.36
ephem==4.1.2
# homeassistant.components.epson
epson-projector==0.5.0
epson-projector==0.5.1
# homeassistant.components.esphome
esphome-dashboard-api==1.2.3
@ -777,7 +777,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20230906.1
home-assistant-frontend==20230908.0
# homeassistant.components.conversation
home-assistant-intents==2023.8.2
@ -927,7 +927,7 @@ micloud==0.5
mill-local==0.2.0
# homeassistant.components.mill
millheater==0.11.1
millheater==0.11.2
# homeassistant.components.minio
minio==7.1.12
@ -1235,7 +1235,7 @@ pyeconet==0.1.20
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==1.9.1
pyenphase==1.11.0
# homeassistant.components.everlights
pyeverlights==0.1.0
@ -1349,7 +1349,7 @@ pylitejet==0.5.0
pylitterbot==2023.4.5
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1
pylutron-caseta==0.18.2
# homeassistant.components.mailgun
pymailgunner==1.4
@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.4.1
pymodbus==3.5.1
# homeassistant.components.monoprice
pymonoprice==0.4
@ -1423,7 +1423,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.10.1
pyoverkiz==1.9.0
# homeassistant.components.openweathermap
pyowm==3.2.0
@ -1729,10 +1729,10 @@ screenlogicpy==0.8.2
securetar==2023.3.0
# homeassistant.components.sense
sense-energy==0.12.0
sense-energy==0.12.1
# homeassistant.components.emulated_kasa
sense_energy==0.12.0
sense_energy==0.12.1
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.0
@ -2036,7 +2036,7 @@ youtubeaio==1.1.5
zamg==0.3.0
# homeassistant.components.zeroconf
zeroconf==0.91.1
zeroconf==0.98.0
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -2471,6 +2471,75 @@ async def test_thermostat(hass: HomeAssistant) -> None:
assert call.data["preset_mode"] == "eco"
async def test_no_current_target_temp_adjusting_temp(hass: HomeAssistant) -> None:
"""Test thermostat adjusting temp with no initial target temperature."""
hass.config.units = US_CUSTOMARY_SYSTEM
device = (
"climate.test_thermostat",
"cool",
{
"temperature": None,
"target_temp_high": None,
"target_temp_low": None,
"current_temperature": 75.0,
"friendly_name": "Test Thermostat",
"supported_features": 1 | 2 | 4 | 128,
"hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"],
"preset_mode": None,
"preset_modes": ["eco"],
"min_temp": 50,
"max_temp": 90,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "climate#test_thermostat"
assert appliance["displayCategories"][0] == "THERMOSTAT"
assert appliance["friendlyName"] == "Test Thermostat"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.PowerController",
"Alexa.ThermostatController",
"Alexa.TemperatureSensor",
"Alexa.EndpointHealth",
"Alexa",
)
properties = await reported_properties(hass, "climate#test_thermostat")
properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL")
properties.assert_not_has_property(
"Alexa.ThermostatController",
"targetSetpoint",
)
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"}
)
thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController")
assert thermostat_capability is not None
configuration = thermostat_capability["configuration"]
assert configuration["supportsScheduling"] is False
supported_modes = ["OFF", "HEAT", "COOL", "AUTO", "ECO", "CUSTOM"]
for mode in supported_modes:
assert mode in configuration["supportedModes"]
# Adjust temperature where target temp is not set
msg = await assert_request_fails(
"Alexa.ThermostatController",
"AdjustTargetTemperature",
"climate#test_thermostat",
"climate.set_temperature",
hass,
payload={"targetSetpointDelta": {"value": -5.0, "scale": "KELVIN"}},
)
assert msg["event"]["payload"]["type"] == "INVALID_TARGET_STATE"
assert msg["event"]["payload"]["message"] == (
"The current target temperature is not set, cannot adjust target temperature"
)
async def test_thermostat_dual(hass: HomeAssistant) -> None:
"""Test thermostat discovery with auto mode, with upper and lower target temperatures."""
hass.config.units = US_CUSTOMARY_SYSTEM

View File

@ -96,6 +96,28 @@ async def test_api_state_change_of_non_existing_entity(
assert hass.states.get("test_entity.that_does_not_exist").state == new_state
async def test_api_state_change_with_bad_entity_id(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test if API sends appropriate error if we omit state."""
resp = await mock_api_client.post(
"/api/states/bad.entity.id", json={"state": "new_state"}
)
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_api_state_change_with_bad_state(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test if API sends appropriate error if we omit state."""
resp = await mock_api_client.post(
"/api/states/test.test", json={"state": "x" * 256}
)
assert resp.status == HTTPStatus.BAD_REQUEST
async def test_api_state_change_with_bad_data(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:

View File

@ -0,0 +1,27 @@
"""Common methods used across tests for Freebox."""
from unittest.mock import patch
from homeassistant.components.freebox.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import MOCK_HOST, MOCK_PORT
from tests.common import MockConfigEntry
async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry:
"""Set up the Freebox platform."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
unique_id=MOCK_HOST,
)
mock_entry.add_to_hass(hass)
with patch("homeassistant.components.freebox.PLATFORMS", [platform]):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
return mock_entry

View File

@ -1986,7 +1986,7 @@ DATA_HOME_GET_NODES = [
"category": "kfb",
"group": {"label": ""},
"id": 9,
"label": "Télécommande I",
"label": "Télécommande",
"name": "node_9",
"props": {
"Address": 5,
@ -2067,7 +2067,7 @@ DATA_HOME_GET_NODES = [
"category": "dws",
"group": {"label": "Entrée"},
"id": 11,
"label": "dws i",
"label": "Ouverture porte",
"name": "node_11",
"props": {
"Address": 6,
@ -2259,7 +2259,7 @@ DATA_HOME_GET_NODES = [
"category": "pir",
"group": {"label": "Salon"},
"id": 26,
"label": "Salon Détecteur s",
"label": "Détecteur",
"name": "node_26",
"props": {
"Address": 9,

View File

@ -0,0 +1,71 @@
"""Tests for the Freebox sensors."""
from copy import deepcopy
from unittest.mock import Mock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.freebox import SCAN_INTERVAL
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from .common import setup_platform
from .const import DATA_HOME_GET_NODES, DATA_STORAGE_GET_DISKS
from tests.common import async_fire_time_changed
async def test_disk(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock
) -> None:
"""Test disk sensor."""
await setup_platform(hass, SENSOR_DOMAIN)
# Initial state
assert (
router().storage.get_disks.return_value[2]["partitions"][0]["total_bytes"]
== 1960000000000
)
assert (
router().storage.get_disks.return_value[2]["partitions"][0]["free_bytes"]
== 1730000000000
)
assert hass.states.get("sensor.freebox_free_space").state == "88.27"
# Simulate a changed storage size
data_storage_get_disks_changed = deepcopy(DATA_STORAGE_GET_DISKS)
data_storage_get_disks_changed[2]["partitions"][0]["free_bytes"] = 880000000000
router().storage.get_disks.return_value = data_storage_get_disks_changed
# Simulate an update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
# To execute the save
await hass.async_block_till_done()
assert hass.states.get("sensor.freebox_free_space").state == "44.9"
async def test_battery(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock
) -> None:
"""Test battery sensor."""
await setup_platform(hass, SENSOR_DOMAIN)
assert hass.states.get("sensor.telecommande_niveau_de_batterie").state == "100"
assert hass.states.get("sensor.ouverture_porte_niveau_de_batterie").state == "100"
assert hass.states.get("sensor.detecteur_niveau_de_batterie").state == "100"
# Simulate a changed battery
data_home_get_nodes_changed = deepcopy(DATA_HOME_GET_NODES)
data_home_get_nodes_changed[2]["show_endpoints"][3]["value"] = 25
data_home_get_nodes_changed[3]["show_endpoints"][3]["value"] = 50
data_home_get_nodes_changed[4]["show_endpoints"][3]["value"] = 75
router().home.get_home_nodes.return_value = data_home_get_nodes_changed
# Simulate an update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
# To execute the save
await hass.async_block_till_done()
assert hass.states.get("sensor.telecommande_niveau_de_batterie").state == "25"
assert hass.states.get("sensor.ouverture_porte_niveau_de_batterie").state == "50"
assert hass.states.get("sensor.detecteur_niveau_de_batterie").state == "75"

View File

@ -626,6 +626,16 @@ async def test_battery_sensor_state_via_mqtt(
"unit_of_measurement": "%",
}
# Test polled state update
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/stat/STATUS11",
'{"StatusSTS":{"BatteryPercentage":50}}',
)
await hass.async_block_till_done()
state = hass.states.get("sensor.tasmota_battery_level")
assert state.state == "50"
@pytest.mark.parametrize("status_sensor_disabled", [False])
async def test_single_shot_status_sensor_state_via_mqtt(

View File

@ -1,7 +1,6 @@
"""Test the thread websocket API."""
import dataclasses
import time
from unittest.mock import Mock, patch
import pytest
@ -191,50 +190,49 @@ async def test_diagnostics(
"""Test diagnostics for thread routers."""
cache = mock_async_zeroconf.zeroconf.cache = DNSCache()
now = time.monotonic() * 1000
cache.async_add_records(
[
*TEST_ZEROCONF_RECORD_1.dns_addresses(created=now),
TEST_ZEROCONF_RECORD_1.dns_service(created=now),
TEST_ZEROCONF_RECORD_1.dns_text(created=now),
TEST_ZEROCONF_RECORD_1.dns_pointer(created=now),
*TEST_ZEROCONF_RECORD_1.dns_addresses(),
TEST_ZEROCONF_RECORD_1.dns_service(),
TEST_ZEROCONF_RECORD_1.dns_text(),
TEST_ZEROCONF_RECORD_1.dns_pointer(),
]
)
cache.async_add_records(
[
*TEST_ZEROCONF_RECORD_2.dns_addresses(created=now),
TEST_ZEROCONF_RECORD_2.dns_service(created=now),
TEST_ZEROCONF_RECORD_2.dns_text(created=now),
TEST_ZEROCONF_RECORD_2.dns_pointer(created=now),
*TEST_ZEROCONF_RECORD_2.dns_addresses(),
TEST_ZEROCONF_RECORD_2.dns_service(),
TEST_ZEROCONF_RECORD_2.dns_text(),
TEST_ZEROCONF_RECORD_2.dns_pointer(),
]
)
# Test for invalid cache
cache.async_add_records([TEST_ZEROCONF_RECORD_3.dns_pointer(created=now)])
cache.async_add_records([TEST_ZEROCONF_RECORD_3.dns_pointer()])
# Test for invalid record
cache.async_add_records(
[
*TEST_ZEROCONF_RECORD_4.dns_addresses(created=now),
TEST_ZEROCONF_RECORD_4.dns_service(created=now),
TEST_ZEROCONF_RECORD_4.dns_text(created=now),
TEST_ZEROCONF_RECORD_4.dns_pointer(created=now),
*TEST_ZEROCONF_RECORD_4.dns_addresses(),
TEST_ZEROCONF_RECORD_4.dns_service(),
TEST_ZEROCONF_RECORD_4.dns_text(),
TEST_ZEROCONF_RECORD_4.dns_pointer(),
]
)
# Test for record without xa
cache.async_add_records(
[
*TEST_ZEROCONF_RECORD_5.dns_addresses(created=now),
TEST_ZEROCONF_RECORD_5.dns_service(created=now),
TEST_ZEROCONF_RECORD_5.dns_text(created=now),
TEST_ZEROCONF_RECORD_5.dns_pointer(created=now),
*TEST_ZEROCONF_RECORD_5.dns_addresses(),
TEST_ZEROCONF_RECORD_5.dns_service(),
TEST_ZEROCONF_RECORD_5.dns_text(),
TEST_ZEROCONF_RECORD_5.dns_pointer(),
]
)
# Test for record without xp
cache.async_add_records(
[
*TEST_ZEROCONF_RECORD_6.dns_addresses(created=now),
TEST_ZEROCONF_RECORD_6.dns_service(created=now),
TEST_ZEROCONF_RECORD_6.dns_text(created=now),
TEST_ZEROCONF_RECORD_6.dns_pointer(created=now),
*TEST_ZEROCONF_RECORD_6.dns_addresses(),
TEST_ZEROCONF_RECORD_6.dns_service(),
TEST_ZEROCONF_RECORD_6.dns_text(),
TEST_ZEROCONF_RECORD_6.dns_pointer(),
]
)
assert await async_setup_component(hass, DOMAIN, {})

View File

@ -153,9 +153,66 @@ async def test_legacy_config_entry(hass: HomeAssistant) -> None:
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 30
async def test_v4_weather(hass: HomeAssistant) -> None:
async def test_v4_weather(hass: HomeAssistant, tomorrowio_config_entry_update) -> None:
"""Test v4 weather data."""
weather_state = await _setup(hass, API_V4_ENTRY_DATA)
tomorrowio_config_entry_update.assert_called_with(
[
"temperature",
"humidity",
"pressureSeaLevel",
"windSpeed",
"windDirection",
"weatherCode",
"visibility",
"pollutantO3",
"windGust",
"cloudCover",
"precipitationType",
"pollutantCO",
"mepIndex",
"mepHealthConcern",
"mepPrimaryPollutant",
"cloudBase",
"cloudCeiling",
"cloudCover",
"dewPoint",
"epaIndex",
"epaHealthConcern",
"epaPrimaryPollutant",
"temperatureApparent",
"fireIndex",
"pollutantNO2",
"pollutantO3",
"particulateMatter10",
"particulateMatter25",
"grassIndex",
"treeIndex",
"weedIndex",
"precipitationType",
"pressureSurfaceLevel",
"solarGHI",
"pollutantSO2",
"uvIndex",
"uvHealthConcern",
"windGust",
],
[
"temperatureMin",
"temperatureMax",
"dewPoint",
"humidity",
"windSpeed",
"windDirection",
"weatherCode",
"precipitationIntensityAvg",
"precipitationProbability",
],
nowcast_timestep=60,
location="80.0,80.0",
)
assert weather_state.state == ATTR_CONDITION_SUNNY
assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
assert len(weather_state.attributes[ATTR_FORECAST]) == 14

View File

@ -78,7 +78,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) ->
# Should be recoverable after hits error
with patch(
"homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices",
"homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_devices_data",
return_value={
"wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G",
"ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;",
@ -191,7 +191,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) ->
# Should be recoverable after hits error
with patch(
"homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices",
"homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_devices_data",
return_value={
"wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G",
"ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;",

View File

@ -1234,27 +1234,27 @@ EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False}
ERR_MSG = {"type": "result", "success": False}
VARIABLE_ERROR_UNDEFINED_FUNC = {
EVENT_UNDEFINED_FUNC_1 = {
"error": "'my_unknown_func' is undefined",
"level": "ERROR",
}
TEMPLATE_ERROR_UNDEFINED_FUNC = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_func' is undefined",
EVENT_UNDEFINED_FUNC_2 = {
"error": "UndefinedError: 'my_unknown_func' is undefined",
"level": "ERROR",
}
VARIABLE_WARNING_UNDEFINED_VAR = {
EVENT_UNDEFINED_VAR_WARN = {
"error": "'my_unknown_var' is undefined",
"level": "WARNING",
}
TEMPLATE_ERROR_UNDEFINED_VAR = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_var' is undefined",
EVENT_UNDEFINED_VAR_ERR = {
"error": "UndefinedError: 'my_unknown_var' is undefined",
"level": "ERROR",
}
TEMPLATE_ERROR_UNDEFINED_FILTER = {
"code": "template_error",
"message": "TemplateAssertionError: No filter named 'unknown_filter'.",
EVENT_UNDEFINED_FILTER = {
"error": "TemplateAssertionError: No filter named 'unknown_filter'.",
"level": "ERROR",
}
@ -1264,16 +1264,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1282,11 +1285,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1325,16 +1336,20 @@ async def test_render_template_with_error(
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1343,11 +1358,19 @@ async def test_render_template_with_error(
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1386,19 +1409,35 @@ async def test_render_template_with_timeout_and_error(
[
(
"{{ my_unknown_func() + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}],
[
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
],
),
(
"{{ my_unknown_var }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1409,7 +1448,73 @@ async def test_render_template_strict_with_timeout_and_error(
template: str,
expected_events: list[dict[str, str]],
) -> None:
"""Test a template with an error with a timeout."""
"""Test a template with an error with a timeout.
In this test report_errors is enabled.
"""
caplog.set_level(logging.INFO)
await websocket_client.send_json(
{
"id": 5,
"type": "render_template",
"template": template,
"timeout": 5,
"strict": True,
"report_errors": True,
}
)
for expected_event in expected_events:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
@pytest.mark.parametrize(
("template", "expected_events"),
[
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ my_unknown_var + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ now() | unknown_filter }}",
[
{"type": "result", "success": True, "result": None},
],
),
],
)
async def test_render_template_strict_with_timeout_and_error_2(
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events: list[dict[str, str]],
) -> None:
"""Test a template with an error with a timeout.
In this test report_errors is disabled.
"""
caplog.set_level(logging.INFO)
await websocket_client.send_json(
{
@ -1427,30 +1532,164 @@ async def test_render_template_strict_with_timeout_and_error(
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
assert "TemplateError" in caplog.text
@pytest.mark.parametrize(
("template", "expected_events_1", "expected_events_2"),
[
(
"{{ now() | random }}",
[
{
"type": "event",
"event": {
"error": "TypeError: object of type 'datetime.datetime' has no len()",
"level": "ERROR",
},
},
{"type": "result", "success": True, "result": None},
{
"type": "event",
"event": {
"error": "TypeError: object of type 'datetime.datetime' has no len()",
"level": "ERROR",
},
},
],
[],
),
(
"{{ float(states.sensor.foo.state) + 1 }}",
[
{
"type": "event",
"event": {
"error": "UndefinedError: 'None' has no attribute 'state'",
"level": "ERROR",
},
},
{"type": "result", "success": True, "result": None},
{
"type": "event",
"event": {
"error": "UndefinedError: 'None' has no attribute 'state'",
"level": "ERROR",
},
},
],
[
{
"type": "event",
"event": {
"result": 3.0,
"listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]},
},
},
],
),
],
)
async def test_render_template_error_in_template_code(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events_1: list[dict[str, str]],
expected_events_2: list[dict[str, str]],
) -> None:
"""Test a template that will throw in template.py."""
"""Test a template that will throw in template.py.
In this test report_errors is enabled.
"""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": "{{ now() | random }}"}
{
"id": 5,
"type": "render_template",
"template": template,
"report_errors": True,
}
)
for expected_event in expected_events_1:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
for key, value in expected_event.items():
assert msg[key] == value
hass.states.async_set("sensor.foo", "2")
for expected_event in expected_events_2:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
@pytest.mark.parametrize(
("template", "expected_events_1", "expected_events_2"),
[
(
"{{ now() | random }}",
[
{"type": "result", "success": True, "result": None},
],
[],
),
(
"{{ float(states.sensor.foo.state) + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
[
{
"type": "event",
"event": {
"result": 3.0,
"listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]},
},
},
],
),
],
)
async def test_render_template_error_in_template_code_2(
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events_1: list[dict[str, str]],
expected_events_2: list[dict[str, str]],
) -> None:
"""Test a template that will throw in template.py.
In this test report_errors is disabled.
"""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": template}
)
for expected_event in expected_events_1:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
hass.states.async_set("sensor.foo", "2")
for expected_event in expected_events_2:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "TemplateError" in caplog.text
async def test_render_template_with_delayed_error(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
) -> None:
@ -1512,7 +1751,10 @@ async def test_render_template_with_delayed_error(
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"error": "UndefinedError: 'explode' is undefined"}
assert event == {
"error": "UndefinedError: 'explode' is undefined",
"level": "ERROR",
}
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text

View File

@ -3239,27 +3239,6 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(
]
async def test_async_track_template_result_raise_on_template_error(
hass: HomeAssistant,
) -> None:
"""Test that we raise as soon as we encounter a failed template."""
with pytest.raises(TemplateError):
async_track_template_result(
hass,
[
TrackTemplate(
Template(
"{{ states.switch | function_that_does_not_exist | list }}"
),
None,
),
],
ha.callback(lambda event, updates: None),
raise_on_template_error=True,
)
async def test_track_template_with_time(hass: HomeAssistant) -> None:
"""Test tracking template with time."""