This commit is contained in:
Franck Nijhof 2023-10-06 20:36:37 +02:00 committed by GitHub
commit a6edfa85b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 651 additions and 180 deletions

View File

@ -9,7 +9,7 @@ from homeassistant.components import stt
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DATA_CONFIG, DOMAIN
from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DOMAIN
from .error import PipelineNotFound
from .pipeline import (
AudioSettings,
@ -45,7 +45,9 @@ __all__ = (
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Optional("debug_recording_dir"): str},
{
vol.Optional(CONF_DEBUG_RECORDING_DIR): str,
},
)
},
extra=vol.ALLOW_EXTRA,

View File

@ -2,3 +2,12 @@
DOMAIN = "assist_pipeline"
DATA_CONFIG = f"{DOMAIN}.config"
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds
CONF_DEBUG_RECORDING_DIR = "debug_recording_dir"
DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up"
DEFAULT_WAKE_WORD_COOLDOWN = 2 # seconds

View File

@ -48,7 +48,13 @@ from homeassistant.util import (
)
from homeassistant.util.limited_size_dict import LimitedSizeDict
from .const import DATA_CONFIG, DOMAIN
from .const import (
CONF_DEBUG_RECORDING_DIR,
DATA_CONFIG,
DATA_LAST_WAKE_UP,
DEFAULT_WAKE_WORD_COOLDOWN,
DOMAIN,
)
from .error import (
IntentRecognitionError,
PipelineError,
@ -399,6 +405,9 @@ class WakeWordSettings:
audio_seconds_to_buffer: float = 0
"""Seconds of audio to buffer before detection and forward to STT."""
cooldown_seconds: float = DEFAULT_WAKE_WORD_COOLDOWN
"""Seconds after a wake word detection where other detections are ignored."""
@dataclass(frozen=True)
class AudioSettings:
@ -603,6 +612,8 @@ class PipelineRun:
)
)
wake_word_settings = self.wake_word_settings or WakeWordSettings()
# Remove language since it doesn't apply to wake words yet
metadata_dict.pop("language", None)
@ -612,6 +623,7 @@ class PipelineRun:
{
"entity_id": self.wake_word_entity_id,
"metadata": metadata_dict,
"timeout": wake_word_settings.timeout or 0,
},
)
)
@ -619,8 +631,6 @@ class PipelineRun:
if self.debug_recording_queue is not None:
self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_entity_id}")
wake_word_settings = self.wake_word_settings or WakeWordSettings()
wake_word_vad: VoiceActivityTimeout | None = None
if (wake_word_settings.timeout is not None) and (
wake_word_settings.timeout > 0
@ -670,6 +680,17 @@ class PipelineRun:
if result is None:
wake_word_output: dict[str, Any] = {}
else:
# Avoid duplicate detections by checking cooldown
last_wake_up = self.hass.data.get(DATA_LAST_WAKE_UP)
if last_wake_up is not None:
sec_since_last_wake_up = time.monotonic() - last_wake_up
if sec_since_last_wake_up < wake_word_settings.cooldown_seconds:
_LOGGER.debug("Duplicate wake word detection occurred")
raise WakeWordDetectionAborted
# Record last wake up time to block duplicate detections
self.hass.data[DATA_LAST_WAKE_UP] = time.monotonic()
if result.queued_audio:
# Add audio that was pending at detection.
#
@ -1032,7 +1053,7 @@ class PipelineRun:
# Directory to save audio for each pipeline run.
# Configured in YAML for assist_pipeline.
if debug_recording_dir := self.hass.data[DATA_CONFIG].get(
"debug_recording_dir"
CONF_DEBUG_RECORDING_DIR
):
if device_id is None:
# <debug_recording_dir>/<pipeline.name>/<run.id>

View File

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util import language as language_util
from .const import DOMAIN
from .const import DEFAULT_PIPELINE_TIMEOUT, DEFAULT_WAKE_WORD_TIMEOUT, DOMAIN
from .error import PipelineNotFound
from .pipeline import (
AudioSettings,
@ -30,9 +30,6 @@ from .pipeline import (
async_get_pipeline,
)
DEFAULT_TIMEOUT = 60 * 5 # seconds
DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds
_LOGGER = logging.getLogger(__name__)
@ -117,7 +114,7 @@ async def websocket_run(
)
return
timeout = msg.get("timeout", DEFAULT_TIMEOUT)
timeout = msg.get("timeout", DEFAULT_PIPELINE_TIMEOUT)
start_stage = PipelineStage(msg["start_stage"])
end_stage = PipelineStage(msg["end_stage"])
handler_id: int | None = None

View File

@ -286,6 +286,13 @@
"on": "[%key:component::binary_sensor::entity_component::gas::state::on%]"
}
},
"tamper": {
"name": "Tamper",
"state": {
"off": "[%key:component::binary_sensor::entity_component::gas::state::off%]",
"on": "Tampering detected"
}
},
"update": {
"name": "Update",
"state": {

View File

@ -19,6 +19,6 @@
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.12.0",
"dbus-fast==2.11.0"
"dbus-fast==2.11.1"
]
}

View File

@ -540,9 +540,9 @@ class CalendarEntity(Entity):
@callback
def update(_: datetime.datetime) -> None:
"""Run when the active or upcoming event starts or ends."""
"""Update state and reschedule next alarms."""
_LOGGER.debug("Running %s update", self.entity_id)
self._async_write_ha_state()
self.async_write_ha_state()
if now < event.start_datetime_local:
self._alarm_unsubs.append(

View File

@ -24,7 +24,10 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
CONFIG_SCHEMA = vol.Schema(
{vol.Optional(DOMAIN): {}},
extra=vol.ALLOW_EXTRA,
)
# Extend the existing light.turn_on service schema
SERVICE_SCHEMA = vol.All(
@ -62,11 +65,12 @@ def _get_color(file_handler) -> tuple:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Color extractor component."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
if DOMAIN in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
)
)
)
return True

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.5.36"]
"requirements": ["env-canada==0.5.37"]
}

View File

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

View File

@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["pyhap"],
"requirements": [
"HAP-python==4.7.1",
"HAP-python==4.8.0",
"fnv-hash-fast==0.4.1",
"PyQRCode==1.2.1",
"base36==0.1.1"

View File

@ -7,7 +7,7 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/loqed",
"iot_class": "local_push",
"requirements": ["loqedAPI==2.1.7"],
"requirements": ["loqedAPI==2.1.8"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.STRING: ENTRY("s", 1, PARM_IS_LEGAL(True, False, False, False, False)),
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)),
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
}
@ -143,7 +143,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
f"{name}: Size of structure is {size} bytes but `{CONF_COUNT}: {count}` is {bytecount} bytes"
)
else:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
if data_type != DataType.STRING:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
if slave_count:
structure = (
f">{slave_count + 1}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"

View File

@ -52,7 +52,7 @@ DEFAULT_SOURCE_TYPE = SourceType.GPS
def valid_config(config: ConfigType) -> ConfigType:
"""Check if there is a state topic or json_attributes_topic."""
if CONF_STATE_TOPIC not in config and CONF_JSON_ATTRS_TOPIC not in config:
raise vol.MultipleInvalid(
raise vol.Invalid(
f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}"
)
return config

View File

@ -58,7 +58,7 @@ def _get_stop_tags(
# Append directions for stops with shared titles
for tag, title in tags.items():
if title_counts[title] > 1:
tags[tag] = f"{title} ({stop_directions[tag]})"
tags[tag] = f"{title} ({stop_directions.get(tag, tag)})"
return tags

View File

@ -277,7 +277,8 @@ class MinutPointEntity(Entity):
sw_version=device["firmware"]["installed"],
via_device=(DOMAIN, device["home"]),
)
self._attr_name = f"{self._name} {device_class.capitalize()}"
if device_class:
self._attr_name = f"{self._name} {device_class.capitalize()}"
def __str__(self):
"""Return string representation of device."""

View File

@ -61,7 +61,7 @@ class RainBirdCalendarEntity(
"""Create the Calendar event device."""
super().__init__(coordinator)
self._event: CalendarEvent | None = None
if unique_id:
if unique_id is not None:
self._attr_unique_id = unique_id
self._attr_device_info = device_info
else:

View File

@ -51,7 +51,7 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity
) -> None:
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
if coordinator.unique_id:
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-rain-delay"
self._attr_device_info = coordinator.device_info
else:

View File

@ -52,7 +52,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity)
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
if coordinator.unique_id:
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:

View File

@ -65,17 +65,18 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
"""Initialize a Rain Bird Switch Device."""
super().__init__(coordinator)
self._zone = zone
if coordinator.unique_id:
_LOGGER.debug("coordinator.unique_id=%s", coordinator.unique_id)
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{zone}"
device_name = f"{MANUFACTURER} Sprinkler {zone}"
if imported_name:
self._attr_name = imported_name
self._attr_has_entity_name = False
else:
self._attr_name = None if coordinator.unique_id else device_name
self._attr_name = None if coordinator.unique_id is not None else device_name
self._attr_has_entity_name = True
self._duration_minutes = duration_minutes
if coordinator.unique_id and self._attr_unique_id:
if coordinator.unique_id is not None and self._attr_unique_id is not None:
self._attr_device_info = DeviceInfo(
name=device_name,
identifiers={(DOMAIN, self._attr_unique_id)},

View File

@ -1924,7 +1924,13 @@ def get_latest_short_term_statistics(
for metadata_id in missing_metadata_ids
if (
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
run_cache, session, metadata_id
# orm_rows=False is used here because we are in
# a read-only session, and there will never be
# any pending inserts in the session.
run_cache,
session,
metadata_id,
orm_rows=False,
)
)
is not None
@ -2310,8 +2316,14 @@ def _import_statistics_with_session(
# We just inserted new short term statistics, so we need to update the
# ShortTermStatisticsRunCache with the latest id for the metadata_id
run_cache = get_short_term_statistics_run_cache(instance.hass)
#
# Because we are in the same session and we want to read rows
# that have not been flushed yet, we need to pass orm_rows=True
# to cache_latest_short_term_statistic_id_for_metadata_id
# to ensure that it gets the rows that were just inserted
#
cache_latest_short_term_statistic_id_for_metadata_id(
run_cache, session, metadata_id
run_cache, session, metadata_id, orm_rows=True
)
return True
@ -2326,7 +2338,10 @@ def get_short_term_statistics_run_cache(
def cache_latest_short_term_statistic_id_for_metadata_id(
run_cache: ShortTermStatisticsRunCache, session: Session, metadata_id: int
run_cache: ShortTermStatisticsRunCache,
session: Session,
metadata_id: int,
orm_rows: bool,
) -> int | None:
"""Cache the latest short term statistic for a given metadata_id.
@ -2339,7 +2354,11 @@ def cache_latest_short_term_statistic_id_for_metadata_id(
execute_stmt_lambda_element(
session,
_find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id),
orm_rows=False,
orm_rows=orm_rows
# _import_statistics_with_session needs to be able
# to read back the rows it just inserted without
# a flush so we have to pass orm_rows so we get
# back the latest data.
),
):
id_: int = latest[0].id

View File

@ -139,6 +139,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
"pv_isolation_resistance": SensorEntityDescription(
key="pv_isolation_resistance",
name="PV Isolation Resistance",
native_unit_of_measurement="kOhms",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
"insulation_residual_current": SensorEntityDescription(
key="insulation_residual_current",
name="Insulation Residual Current",
@ -147,6 +154,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
"pv_power": SensorEntityDescription(
key="pv_power",
name="PV Power",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
"grid_power": SensorEntityDescription(
key="grid_power",
name="Grid Power",
@ -479,6 +493,30 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
"sps_voltage": SensorEntityDescription(
key="sps_voltage",
name="Secure Power Supply Voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
entity_registry_enabled_default=False,
),
"sps_current": SensorEntityDescription(
key="sps_current",
name="Secure Power Supply Current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
"sps_power": SensorEntityDescription(
key="sps_power",
name="Secure Power Supply Power",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=False,
),
"optimizer_power": SensorEntityDescription(
key="optimizer_power",
name="Optimizer Power",

View File

@ -35,6 +35,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Validate input from user input."""
errors: dict[str, str] = {}
camera_info: CameraInfo | None = None
camera_location: str | None = None
web_session = async_get_clientsession(self.hass)
camera_api = TrafikverketCamera(web_session, sensor_api)
@ -49,7 +50,12 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except UnknownError:
errors["base"] = "cannot_connect"
camera_location = camera_info.location if camera_info else None
if camera_info:
if _location := camera_info.location:
camera_location = _location
else:
camera_location = camera_info.camera_name
return (errors, camera_location)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:

View File

@ -1,13 +1,19 @@
"""The waze_travel_time component."""
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN, SEMAPHORE
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Load the saved entities."""
if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}):
hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True

View File

@ -2,6 +2,7 @@
from __future__ import annotations
DOMAIN = "waze_travel_time"
SEMAPHORE = "semaphore"
CONF_DESTINATION = "destination"
CONF_ORIGIN = "origin"

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
"iot_class": "cloud_polling",
"loggers": ["pywaze", "homeassistant.helpers.location"],
"requirements": ["pywaze==0.5.0"]
"requirements": ["pywaze==0.5.1"]
}

View File

@ -43,6 +43,7 @@ from .const import (
DEFAULT_NAME,
DOMAIN,
IMPERIAL_UNITS,
SEMAPHORE,
)
_LOGGER = logging.getLogger(__name__)
@ -51,7 +52,7 @@ SCAN_INTERVAL = timedelta(minutes=5)
PARALLEL_UPDATES = 1
MS_BETWEEN_API_CALLS = 0.5
SECONDS_BETWEEN_API_CALLS = 0.5
async def async_setup_entry(
@ -148,8 +149,12 @@ class WazeTravelTime(SensorEntity):
_LOGGER.debug("Fetching Route for %s", self._attr_name)
self._waze_data.origin = find_coordinates(self.hass, self._origin)
self._waze_data.destination = find_coordinates(self.hass, self._destination)
await self._waze_data.async_update()
await asyncio.sleep(MS_BETWEEN_API_CALLS)
await self.hass.data[DOMAIN][SEMAPHORE].acquire()
try:
await self._waze_data.async_update()
await asyncio.sleep(SECONDS_BETWEEN_API_CALLS)
finally:
self.hass.data[DOMAIN][SEMAPHORE].release()
class WazeTravelTimeData:

View File

@ -245,7 +245,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
translation_key="wind_gust",
icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
@ -255,7 +255,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
translation_key="wind_lull",
icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
@ -265,17 +265,17 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.WIND_SPEED,
icon="mdi:weather-windy",
event_subscriptions=[EVENT_RAPID_WIND, EVENT_OBSERVATION],
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
),
WeatherFlowSensorEntityDescription(
key="wind_speed_average",
key="wind_average",
translation_key="wind_speed_average",
icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude,

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
from __future__ import annotations
from collections.abc import Awaitable, Callable
import contextlib
from typing import Any
from aiohttp.hdrs import METH_HEAD, METH_POST
@ -37,7 +38,6 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType
from .api import ConfigEntryWithingsApi
@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
webhook_name = "Withings"
if entry.title != DEFAULT_TITLE:
webhook_name = " ".join([DEFAULT_TITLE, entry.title])
webhook_name = f"{DEFAULT_TITLE} {entry.title}"
webhook_register(
hass,
@ -182,14 +182,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED:
await unregister_webhook(None)
async_call_later(hass, 30, register_webhook)
entry.async_on_unload(async_call_later(hass, 30, register_webhook))
if cloud.async_active_subscription(hass):
if cloud.async_is_connected(hass):
await register_webhook(None)
cloud.async_listen_connection_change(hass, manage_cloudhook)
entry.async_on_unload(async_call_later(hass, 1, register_webhook))
entry.async_on_unload(
cloud.async_listen_connection_change(hass, manage_cloudhook)
)
else:
async_at_started(hass, register_webhook)
entry.async_on_unload(async_call_later(hass, 1, register_webhook))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
@ -214,9 +216,12 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
"""Generate the full URL for a webhook_id."""
if CONF_CLOUDHOOK_URL not in entry.data:
webhook_url = await cloud.async_create_cloudhook(
hass, entry.data[CONF_WEBHOOK_ID]
)
webhook_id = entry.data[CONF_WEBHOOK_ID]
# Some users already have their webhook as cloudhook.
# We remove them to be sure we can create a new one.
with contextlib.suppress(ValueError):
await cloud.async_delete_cloudhook(hass, webhook_id)
webhook_url = await cloud.async_create_cloudhook(hass, webhook_id)
data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
hass.config_entries.async_update_entry(entry, data=data)
return webhook_url

View File

@ -240,8 +240,7 @@ SENSORS = [
key=Measurement.SLEEP_HEART_RATE_MAX.value,
measurement=Measurement.SLEEP_HEART_RATE_MAX,
measure_type=GetSleepSummaryField.HR_MAX,
translation_key="fat_mass",
name="Maximum heart rate",
translation_key="maximum_heart_rate",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
@ -251,7 +250,7 @@ SENSORS = [
key=Measurement.SLEEP_HEART_RATE_MIN.value,
measurement=Measurement.SLEEP_HEART_RATE_MIN,
measure_type=GetSleepSummaryField.HR_MIN,
translation_key="maximum_heart_rate",
translation_key="minimum_heart_rate",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,

View File

@ -89,6 +89,9 @@
"maximum_heart_rate": {
"name": "Maximum heart rate"
},
"minimum_heart_rate": {
"name": "Minimum heart rate"
},
"light_sleep": {
"name": "Light sleep"
},

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/yardian",
"iot_class": "local_polling",
"requirements": ["pyyardian==1.1.0"]
"requirements": ["pyyardian==1.1.1"]
}

View File

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

View File

@ -139,6 +139,19 @@ def get_endpoint_cluster_attr_data(zha_device: ZHADevice) -> dict:
def get_cluster_attr_data(cluster: Cluster) -> dict:
"""Return cluster attribute data."""
unsupported_attributes = {}
for u_attr in cluster.unsupported_attributes:
try:
u_attr_def = cluster.find_attribute(u_attr)
unsupported_attributes[f"0x{u_attr_def.id:04x}"] = {
ATTR_ATTRIBUTE_NAME: u_attr_def.name
}
except KeyError:
if isinstance(u_attr, int):
unsupported_attributes[f"0x{u_attr:04x}"] = {}
else:
unsupported_attributes[u_attr] = {}
return {
ATTRIBUTES: {
f"0x{attr_id:04x}": {
@ -148,10 +161,5 @@ def get_cluster_attr_data(cluster: Cluster) -> dict:
for attr_id, attr_def in cluster.attributes.items()
if (attr_value := cluster.get(attr_def.name)) is not None
},
UNSUPPORTED_ATTRIBUTES: {
f"0x{cluster.find_attribute(u_attr).id:04x}": {
ATTR_ATTRIBUTE_NAME: cluster.find_attribute(u_attr).name
}
for u_attr in cluster.unsupported_attributes
},
UNSUPPORTED_ATTRIBUTES: unsupported_attributes,
}

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 10
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

@ -15,13 +15,13 @@ bluetooth-data-tools==1.12.0
certifi>=2021.5.30
ciso8601==2.3.0
cryptography==41.0.4
dbus-fast==2.11.0
dbus-fast==2.11.1
fnv-hash-fast==0.4.1
ha-av==10.1.1
hass-nabucasa==0.71.0
hassil==1.2.5
home-assistant-bluetooth==1.10.3
home-assistant-frontend==20231002.0
home-assistant-frontend==20231005.0
home-assistant-intents==2023.10.2
httpx==0.24.1
ifaddr==0.2.0
@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtc-noise-gain==1.2.3
yarl==1.9.2
zeroconf==0.115.1
zeroconf==0.115.2
# 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.10.0"
version = "2023.10.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -437,7 +437,7 @@ filterwarnings = [
# -- design choice 3rd party
# https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19
"ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util",
# https://github.com/michaeldavie/env_canada/blob/v0.5.36/env_canada/ec_cache.py
# https://github.com/michaeldavie/env_canada/blob/v0.5.37/env_canada/ec_cache.py
"ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache",
# https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",

View File

@ -26,7 +26,7 @@ CO2Signal==0.4.2
DoorBirdPy==2.1.0
# homeassistant.components.homekit
HAP-python==4.7.1
HAP-python==4.8.0
# homeassistant.components.tasmota
HATasmota==0.7.3
@ -645,7 +645,7 @@ datadog==0.15.0
datapoint==0.9.8
# homeassistant.components.bluetooth
dbus-fast==2.11.0
dbus-fast==2.11.1
# homeassistant.components.debugpy
debugpy==1.8.0
@ -749,7 +749,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env-canada==0.5.36
env-canada==0.5.37
# homeassistant.components.season
ephem==4.1.2
@ -997,7 +997,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20231002.0
home-assistant-frontend==20231005.0
# homeassistant.components.conversation
home-assistant-intents==2023.10.2
@ -1165,7 +1165,7 @@ logi-circle==0.2.3
london-tube-status==0.5
# homeassistant.components.loqed
loqedAPI==2.1.7
loqedAPI==2.1.8
# homeassistant.components.luftdaten
luftdaten==0.7.4
@ -2246,7 +2246,7 @@ pyvlx==0.2.20
pyvolumio==0.1.5
# homeassistant.components.waze_travel_time
pywaze==0.5.0
pywaze==0.5.1
# homeassistant.components.weatherflow
pyweatherflowudp==1.4.3
@ -2270,7 +2270,7 @@ pyws66i==1.1
pyxeoma==1.4.1
# homeassistant.components.yardian
pyyardian==1.1.0
pyyardian==1.1.1
# homeassistant.components.qrcode
pyzbar==0.1.7
@ -2784,7 +2784,7 @@ zamg==0.3.0
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.115.1
zeroconf==0.115.2
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -25,7 +25,7 @@ CO2Signal==0.4.2
DoorBirdPy==2.1.0
# homeassistant.components.homekit
HAP-python==4.7.1
HAP-python==4.8.0
# homeassistant.components.tasmota
HATasmota==0.7.3
@ -528,7 +528,7 @@ datadog==0.15.0
datapoint==0.9.8
# homeassistant.components.bluetooth
dbus-fast==2.11.0
dbus-fast==2.11.1
# homeassistant.components.debugpy
debugpy==1.8.0
@ -605,7 +605,7 @@ energyzero==0.5.0
enocean==0.50
# homeassistant.components.environment_canada
env-canada==0.5.36
env-canada==0.5.37
# homeassistant.components.season
ephem==4.1.2
@ -786,7 +786,7 @@ hole==0.8.0
holidays==0.28
# homeassistant.components.frontend
home-assistant-frontend==20231002.0
home-assistant-frontend==20231005.0
# homeassistant.components.conversation
home-assistant-intents==2023.10.2
@ -903,7 +903,7 @@ logi-circle==0.2.3
london-tube-status==0.5
# homeassistant.components.loqed
loqedAPI==2.1.7
loqedAPI==2.1.8
# homeassistant.components.luftdaten
luftdaten==0.7.4
@ -1672,7 +1672,7 @@ pyvizio==0.1.61
pyvolumio==0.1.5
# homeassistant.components.waze_travel_time
pywaze==0.5.0
pywaze==0.5.1
# homeassistant.components.weatherflow
pyweatherflowudp==1.4.3
@ -1693,7 +1693,7 @@ pywizlight==0.5.14
pyws66i==1.1
# homeassistant.components.yardian
pyyardian==1.1.0
pyyardian==1.1.1
# homeassistant.components.zerproc
pyzerproc==0.4.8
@ -2078,7 +2078,7 @@ yt-dlp==2023.9.24
zamg==0.3.0
# homeassistant.components.zeroconf
zeroconf==0.115.1
zeroconf==0.115.2
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -285,6 +285,7 @@
'format': <AudioFormats.WAV: 'wav'>,
'sample_rate': <AudioSampleRates.SAMPLERATE_16000: 16000>,
}),
'timeout': 0,
}),
'type': <PipelineEventType.WAKE_WORD_START: 'wake_word-start'>,
}),
@ -396,6 +397,7 @@
'format': <AudioFormats.WAV: 'wav'>,
'sample_rate': <AudioSampleRates.SAMPLERATE_16000: 16000>,
}),
'timeout': 0,
}),
'type': <PipelineEventType.WAKE_WORD_START: 'wake_word-start'>,
}),

View File

@ -373,6 +373,7 @@
'format': 'wav',
'sample_rate': 16000,
}),
'timeout': 0,
})
# ---
# name: test_audio_pipeline_with_wake_word_no_timeout.2
@ -474,6 +475,7 @@
'format': 'wav',
'sample_rate': 16000,
}),
'timeout': 1,
})
# ---
# name: test_audio_pipeline_with_wake_word_timeout.2
@ -655,3 +657,63 @@
# name: test_tts_failed.2
None
# ---
# name: test_wake_word_cooldown
dict({
'language': 'en',
'pipeline': <ANY>,
'runner_data': dict({
'stt_binary_handler_id': 1,
'timeout': 300,
}),
})
# ---
# name: test_wake_word_cooldown.1
dict({
'language': 'en',
'pipeline': <ANY>,
'runner_data': dict({
'stt_binary_handler_id': 1,
'timeout': 300,
}),
})
# ---
# name: test_wake_word_cooldown.2
dict({
'entity_id': 'wake_word.test',
'metadata': dict({
'bit_rate': 16,
'channel': 1,
'codec': 'pcm',
'format': 'wav',
'sample_rate': 16000,
}),
'timeout': 3,
})
# ---
# name: test_wake_word_cooldown.3
dict({
'entity_id': 'wake_word.test',
'metadata': dict({
'bit_rate': 16,
'channel': 1,
'codec': 'pcm',
'format': 'wav',
'sample_rate': 16000,
}),
'timeout': 3,
})
# ---
# name: test_wake_word_cooldown.4
dict({
'wake_word_output': dict({
'timestamp': 0,
'wake_word_id': 'test_ww',
}),
})
# ---
# name: test_wake_word_cooldown.5
dict({
'code': 'wake_word_detection_aborted',
'message': '',
})
# ---

View File

@ -10,6 +10,10 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import assist_pipeline, stt
from homeassistant.components.assist_pipeline.const import (
CONF_DEBUG_RECORDING_DIR,
DOMAIN,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.setup import async_setup_component
@ -395,8 +399,8 @@ async def test_pipeline_save_audio(
temp_dir = Path(temp_dir_str)
assert await async_setup_component(
hass,
"assist_pipeline",
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}},
DOMAIN,
{DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
)
pipeline = assist_pipeline.async_get_pipeline(hass)
@ -476,8 +480,8 @@ async def test_pipeline_saved_audio_with_device_id(
temp_dir = Path(temp_dir_str)
assert await async_setup_component(
hass,
"assist_pipeline",
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}},
DOMAIN,
{DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
)
def event_callback(event: assist_pipeline.PipelineEvent):
@ -529,8 +533,8 @@ async def test_pipeline_saved_audio_write_error(
temp_dir = Path(temp_dir_str)
assert await async_setup_component(
hass,
"assist_pipeline",
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}},
DOMAIN,
{DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
)
def event_callback(event: assist_pipeline.PipelineEvent):

View File

@ -9,6 +9,8 @@ from homeassistant.components.assist_pipeline.pipeline import Pipeline, Pipeline
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .conftest import MockWakeWordEntity
from tests.typing import WebSocketGenerator
@ -266,7 +268,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout(
events.append(msg["event"])
# "audio"
await client.send_bytes(bytes([1]) + b"wake word")
await client.send_bytes(bytes([handler_id]) + b"wake word")
msg = await client.receive_json()
assert msg["event"]["type"] == "wake_word-end"
@ -1805,3 +1807,84 @@ async def test_audio_pipeline_with_enhancements(
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"events": events}
async def test_wake_word_cooldown(
hass: HomeAssistant,
init_components,
mock_wake_word_provider_entity: MockWakeWordEntity,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test that duplicate wake word detections are blocked during the cooldown period."""
client_1 = await hass_ws_client(hass)
client_2 = await hass_ws_client(hass)
await client_1.send_json_auto_id(
{
"type": "assist_pipeline/run",
"start_stage": "wake_word",
"end_stage": "tts",
"input": {
"sample_rate": 16000,
"no_vad": True,
"no_chunking": True,
},
}
)
await client_2.send_json_auto_id(
{
"type": "assist_pipeline/run",
"start_stage": "wake_word",
"end_stage": "tts",
"input": {
"sample_rate": 16000,
"no_vad": True,
"no_chunking": True,
},
}
)
# result
msg = await client_1.receive_json()
assert msg["success"], msg
msg = await client_2.receive_json()
assert msg["success"], msg
# run start
msg = await client_1.receive_json()
assert msg["event"]["type"] == "run-start"
msg["event"]["data"]["pipeline"] = ANY
handler_id_1 = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"]
assert msg["event"]["data"] == snapshot
msg = await client_2.receive_json()
assert msg["event"]["type"] == "run-start"
msg["event"]["data"]["pipeline"] = ANY
handler_id_2 = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"]
assert msg["event"]["data"] == snapshot
# wake_word
msg = await client_1.receive_json()
assert msg["event"]["type"] == "wake_word-start"
assert msg["event"]["data"] == snapshot
msg = await client_2.receive_json()
assert msg["event"]["type"] == "wake_word-start"
assert msg["event"]["data"] == snapshot
# Wake both up at the same time
await client_1.send_bytes(bytes([handler_id_1]) + b"wake word")
await client_2.send_bytes(bytes([handler_id_2]) + b"wake word")
# Get response events
msg = await client_1.receive_json()
event_type_1 = msg["event"]["type"]
msg = await client_2.receive_json()
event_type_2 = msg["event"]["type"]
# One should be a wake up, one should be an error
assert {event_type_1, event_type_2} == {"wake_word-end", "error"}

View File

@ -0,0 +1,21 @@
"""Common fixtures for the Color extractor tests."""
import pytest
from homeassistant.components.color_extractor.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
async def config_entry() -> MockConfigEntry:
"""Mock config entry."""
return MockConfigEntry(domain=DOMAIN, data={})
@pytest.fixture
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Add config entry for color extractor."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,17 @@
"""Test Color extractor component setup process."""
from homeassistant.components.color_extractor import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
async def test_legacy_migration(hass: HomeAssistant) -> None:
"""Test migration from yaml to config flow."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) == 1

View File

@ -1,6 +1,7 @@
"""Tests for color_extractor component service calls."""
import base64
import io
from typing import Any
from unittest.mock import Mock, mock_open, patch
import aiohttp
@ -92,15 +93,8 @@ async def setup_light(hass: HomeAssistant):
assert state.state == STATE_OFF
async def test_missing_url_and_path(hass: HomeAssistant) -> None:
async def test_missing_url_and_path(hass: HomeAssistant, setup_integration) -> None:
"""Test that nothing happens when url and path are missing."""
# Load our color_extractor component
await async_setup_component(
hass,
DOMAIN,
{},
)
await hass.async_block_till_done()
# Validate pre service call
state = hass.states.get(LIGHT_ENTITY)
@ -124,15 +118,7 @@ async def test_missing_url_and_path(hass: HomeAssistant) -> None:
assert state.state == STATE_OFF
async def _async_load_color_extractor_url(hass, service_data):
# Load our color_extractor component
await async_setup_component(
hass,
DOMAIN,
{},
)
await hass.async_block_till_done()
async def _async_execute_service(hass: HomeAssistant, service_data: dict[str, Any]):
# Validate pre service call
state = hass.states.get(LIGHT_ENTITY)
assert state
@ -145,7 +131,7 @@ async def _async_load_color_extractor_url(hass, service_data):
async def test_url_success(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None:
"""Test that a successful image GET translate to light RGB."""
service_data = {
@ -158,13 +144,15 @@ async def test_url_success(
# Mock the HTTP Response with a base64 encoded 1x1 pixel
aioclient_mock.get(
url=service_data[ATTR_URL],
content=base64.b64decode(load_fixture("color_extractor_url.txt")),
content=base64.b64decode(
load_fixture("color_extractor/color_extractor_url.txt")
),
)
# Allow access to this URL using the proper mechanism
hass.config.allowlist_external_urls.add("http://example.com/images/")
await _async_load_color_extractor_url(hass, service_data)
await _async_execute_service(hass, service_data)
state = hass.states.get(LIGHT_ENTITY)
assert state
@ -180,7 +168,7 @@ async def test_url_success(
async def test_url_not_allowed(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None:
"""Test that a not allowed external URL fails to turn light on."""
service_data = {
@ -188,7 +176,7 @@ async def test_url_not_allowed(
ATTR_ENTITY_ID: LIGHT_ENTITY,
}
await _async_load_color_extractor_url(hass, service_data)
await _async_execute_service(hass, service_data)
# Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY)
@ -197,7 +185,7 @@ async def test_url_not_allowed(
async def test_url_exception(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None:
"""Test that a HTTPError fails to turn light on."""
service_data = {
@ -211,7 +199,7 @@ async def test_url_exception(
# Mock the HTTP Response with an HTTPError
aioclient_mock.get(url=service_data[ATTR_URL], exc=aiohttp.ClientError)
await _async_load_color_extractor_url(hass, service_data)
await _async_execute_service(hass, service_data)
# Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY)
@ -220,7 +208,7 @@ async def test_url_exception(
async def test_url_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None:
"""Test that a HTTP Error (non 200) doesn't turn light on."""
service_data = {
@ -234,7 +222,7 @@ async def test_url_error(
# Mock the HTTP Response with a 400 Bad Request error
aioclient_mock.get(url=service_data[ATTR_URL], status=400)
await _async_load_color_extractor_url(hass, service_data)
await _async_execute_service(hass, service_data)
# Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY)
@ -244,7 +232,11 @@ async def test_url_error(
@patch(
"builtins.open",
mock_open(read_data=base64.b64decode(load_fixture("color_extractor_file.txt"))),
mock_open(
read_data=base64.b64decode(
load_fixture("color_extractor/color_extractor_file.txt")
)
),
create=True,
)
def _get_file_mock(file_path):
@ -262,7 +254,7 @@ def _get_file_mock(file_path):
@patch("os.path.isfile", Mock(return_value=True))
@patch("os.access", Mock(return_value=True))
async def test_file(hass: HomeAssistant) -> None:
async def test_file(hass: HomeAssistant, setup_integration) -> None:
"""Test that the file only service reads a file and translates to light RGB."""
service_data = {
ATTR_PATH: "/opt/image.png",
@ -274,9 +266,6 @@ async def test_file(hass: HomeAssistant) -> None:
# Add our /opt/ path to the allowed list of paths
hass.config.allowlist_external_dirs.add("/opt/")
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Verify pre service check
state = hass.states.get(LIGHT_ENTITY)
assert state
@ -303,7 +292,7 @@ async def test_file(hass: HomeAssistant) -> None:
@patch("os.path.isfile", Mock(return_value=True))
@patch("os.access", Mock(return_value=True))
async def test_file_denied_dir(hass: HomeAssistant) -> None:
async def test_file_denied_dir(hass: HomeAssistant, setup_integration) -> None:
"""Test that the file only service fails to read an image in a dir not explicitly allowed."""
service_data = {
ATTR_PATH: "/path/to/a/dir/not/allowed/image.png",
@ -312,9 +301,6 @@ async def test_file_denied_dir(hass: HomeAssistant) -> None:
ATTR_BRIGHTNESS_PCT: 100,
}
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Verify pre service check
state = hass.states.get(LIGHT_ENTITY)
assert state

View File

@ -122,6 +122,28 @@ async def test_invalid_json(
assert not mock_dispatcher_send.called
@pytest.mark.no_fail_on_log_exception
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR])
async def test_discovery_schema_error(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test unexpected error JSON config."""
with patch(
"homeassistant.components.mqtt.binary_sensor.DISCOVERY_SCHEMA",
side_effect=AttributeError("Attribute abc not found"),
):
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{"name": "Beer", "state_topic": "ok"}',
)
await hass.async_block_till_done()
assert "AttributeError: Attribute abc not found" in caplog.text
async def test_only_valid_components(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,

View File

@ -19,6 +19,8 @@ def mock_nextbus_lists(mock_nextbus: MagicMock) -> MagicMock:
"stop": [
{"tag": "5650", "title": "Market St & 7th St"},
{"tag": "5651", "title": "Market St & 7th St"},
# Error case test. Duplicate title with no unique direction
{"tag": "5652", "title": "Market St & 7th St"},
],
"direction": [
{

View File

@ -17,6 +17,7 @@ from .conftest import (
PASSWORD,
RAIN_DELAY_OFF,
RAIN_SENSOR_OFF,
SERIAL_NUMBER,
ZONE_3_ON_RESPONSE,
ZONE_5_ON_RESPONSE,
ZONE_OFF_RESPONSE,
@ -286,7 +287,7 @@ async def test_switch_error(
@pytest.mark.parametrize(
("config_entry_unique_id"),
[
None,
(None),
],
)
async def test_no_unique_id(
@ -307,3 +308,34 @@ async def test_no_unique_id(
entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
assert entity_entry is None
@pytest.mark.parametrize(
("config_entry_unique_id", "entity_unique_id"),
[
(SERIAL_NUMBER, "1263613994342-3"),
# Some existing config entries may have a "0" serial number but preserve
# their unique id
(0, "0-3"),
],
)
async def test_has_unique_id(
hass: HomeAssistant,
setup_integration: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
responses: list[AiohttpClientMockResponse],
entity_registry: er.EntityRegistry,
entity_unique_id: str,
) -> None:
"""Test an irrigation switch with no unique id."""
assert await setup_integration()
zone = hass.states.get("switch.rain_bird_sprinkler_3")
assert zone is not None
assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3"
assert zone.state == "off"
entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
assert entity_entry
assert entity_entry.unique_id == entity_unique_id

View File

@ -14,6 +14,7 @@ from homeassistant.components.recorder.db_schema import Statistics, StatisticsSh
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
get_latest_short_term_statistics,
get_metadata,
get_short_term_statistics_run_cache,
list_statistic_ids,
@ -635,6 +636,22 @@ async def test_statistic_during_period(
"change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"])
* 1000,
}
stats = get_latest_short_term_statistics(
hass, {"sensor.test"}, {"last_reset", "max", "mean", "min", "state", "sum"}
)
start = imported_stats_5min[-1]["start"].timestamp()
end = start + (5 * 60)
assert stats == {
"sensor.test": [
{
"end": end,
"last_reset": None,
"start": start,
"state": None,
"sum": 38.0,
}
]
}
@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC))

View File

@ -1,4 +1,12 @@
"""Test the sma sensor platform."""
from pysma.const import (
ENERGY_METER_VIA_INVERTER,
GENERIC_SENSORS,
OPTIMIZERS_VIA_INVERTER,
)
from pysma.definitions import sensor_map
from homeassistant.components.sma.sensor import SENSOR_ENTITIES
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower
from homeassistant.core import HomeAssistant
@ -8,3 +16,15 @@ async def test_sensors(hass: HomeAssistant, init_integration) -> None:
state = hass.states.get("sensor.sma_device_grid_power")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT
async def test_sensor_entities(hass: HomeAssistant, init_integration) -> None:
"""Test SENSOR_ENTITIES contains a SensorEntityDescription for each pysma sensor."""
pysma_sensor_definitions = (
sensor_map[GENERIC_SENSORS]
+ sensor_map[OPTIMIZERS_VIA_INVERTER]
+ sensor_map[ENERGY_METER_VIA_INVERTER]
)
for sensor in pysma_sensor_definitions:
assert sensor.name in SENSOR_ENTITIES

View File

@ -67,3 +67,24 @@ def fixture_get_camera() -> CameraInfo:
status="Running",
camera_type="Road",
)
@pytest.fixture(name="get_camera_no_location")
def fixture_get_camera_no_location() -> CameraInfo:
"""Construct Camera Mock."""
return CameraInfo(
camera_name="Test Camera",
camera_id="1234",
active=True,
deleted=False,
description="Test Camera for testing",
direction="180",
fullsizephoto=True,
location=None,
modified=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC),
phototime=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC),
photourl="https://www.testurl.com/test_photo.jpg",
status="Running",
camera_type="Road",
)

View File

@ -56,6 +56,43 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None:
assert result2["result"].unique_id == "trafikverket_camera-Test location"
async def test_form_no_location_data(
hass: HomeAssistant, get_camera_no_location: CameraInfo
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera",
return_value=get_camera_no_location,
), patch(
"homeassistant.components.trafikverket_camera.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_API_KEY: "1234567890",
CONF_LOCATION: "Test Cam",
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Test Camera"
assert result2["data"] == {
"api_key": "1234567890",
"location": "Test Camera",
}
assert len(mock_setup_entry.mock_calls) == 1
assert result2["result"].unique_id == "trafikverket_camera-Test Camera"
@pytest.mark.parametrize(
("side_effect", "error_key", "base_error"),
[

View File

@ -1,15 +1,17 @@
"""Tests for the withings component."""
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from urllib.parse import urlparse
from aiohttp.test_utils import TestClient
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.webhook import async_generate_url
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
@dataclass
@ -53,3 +55,12 @@ async def setup_integration(
)
await hass.config_entries.async_setup(config_entry.entry_id)
async def prepare_webhook_setup(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Prepare webhooks are registered by waiting a second."""
freezer.tick(timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()

View File

@ -211,13 +211,13 @@
# name: test_all_entities.21
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Fat mass',
'friendly_name': 'henk Maximum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_fat_mass_2',
'entity_id': 'sensor.henk_maximum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '165.0',
@ -226,13 +226,13 @@
# name: test_all_entities.22
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum heart rate',
'friendly_name': 'henk Minimum heart rate',
'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm',
}),
'context': <ANY>,
'entity_id': 'sensor.henk_maximum_heart_rate',
'entity_id': 'sensor.henk_minimum_heart_rate',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '166.0',

View File

@ -2,13 +2,14 @@
from unittest.mock import AsyncMock
from aiohttp.client_exceptions import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from withings_api.common import NotifyAppli
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from . import call_webhook, setup_integration
from . import call_webhook, prepare_webhook_setup, setup_integration
from .conftest import USER_ID, WEBHOOK_ID
from tests.common import MockConfigEntry
@ -20,9 +21,11 @@ async def test_binary_sensor(
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test binary sensor."""
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth()

View File

@ -15,16 +15,11 @@ from homeassistant.components.cloud import CloudNotAvailable
from homeassistant.components.webhook import async_generate_url
from homeassistant.components.withings import CONFIG_SCHEMA, async_setup
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_WEBHOOK_ID,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from . import call_webhook, setup_integration
from . import call_webhook, prepare_webhook_setup, setup_integration
from .conftest import USER_ID, WEBHOOK_ID
from tests.common import (
@ -197,9 +192,12 @@ async def test_webhooks_request_data(
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test calling a webhook requests data."""
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth()
assert withings.async_measure_get_meas.call_count == 1
@ -213,35 +211,6 @@ async def test_webhooks_request_data(
assert withings.async_measure_get_meas.call_count == 2
async def test_delayed_startup(
hass: HomeAssistant,
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test delayed start up."""
hass.state = CoreState.not_running
await setup_integration(hass, webhook_config_entry)
withings.async_notify_subscribe.assert_not_called()
client = await hass_client_no_auth()
assert withings.async_measure_get_meas.call_count == 1
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
await call_webhook(
hass,
WEBHOOK_ID,
{"userid": USER_ID, "appli": NotifyAppli.WEIGHT},
client,
)
assert withings.async_measure_get_meas.call_count == 2
@pytest.mark.parametrize(
"error",
[
@ -395,7 +364,10 @@ async def test_removing_entry_with_cloud_unavailable(
async def test_setup_with_cloud(
hass: HomeAssistant, webhook_config_entry: MockConfigEntry, withings: AsyncMock
hass: HomeAssistant,
webhook_config_entry: MockConfigEntry,
withings: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test if set up with active cloud subscription."""
await mock_cloud(hass)
@ -418,9 +390,12 @@ async def test_setup_with_cloud(
"homeassistant.components.withings.webhook_generate_url"
):
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
assert hass.components.cloud.async_active_subscription() is True
assert hass.components.cloud.async_is_connected() is True
fake_create_cloudhook.assert_called_once()
fake_delete_cloudhook.assert_called_once()
assert (
hass.config_entries.async_entries("withings")[0].data["cloudhook_url"]
@ -432,7 +407,7 @@ async def test_setup_with_cloud(
for config_entry in hass.config_entries.async_entries("withings"):
await hass.config_entries.async_remove(config_entry.entry_id)
fake_delete_cloudhook.assert_called_once()
fake_delete_cloudhook.call_count == 2
await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN)
@ -443,6 +418,7 @@ async def test_setup_without_https(
webhook_config_entry: MockConfigEntry,
withings: AsyncMock,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test if set up with cloud link and without https."""
hass.config.components.add("cloud")
@ -456,6 +432,7 @@ async def test_setup_without_https(
) as mock_async_generate_url:
mock_async_generate_url.return_value = "http://example.com"
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
await hass.async_block_till_done()
mock_async_generate_url.assert_called_once()
@ -491,6 +468,7 @@ async def test_cloud_disconnect(
"homeassistant.components.withings.webhook_generate_url"
):
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
assert hass.components.cloud.async_active_subscription() is True
assert hass.components.cloud.async_is_connected() is True
@ -536,9 +514,11 @@ async def test_webhook_post(
body: dict[str, Any],
expected_code: int,
current_request_with_host: None,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test webhook callback."""
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth()
webhook_url = async_generate_url(hass, WEBHOOK_ID)

View File

@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from . import call_webhook, setup_integration
from . import call_webhook, prepare_webhook_setup, setup_integration
from .conftest import USER_ID, WEBHOOK_ID
from tests.common import MockConfigEntry, async_fire_time_changed
@ -96,9 +96,11 @@ async def test_sensor_default_enabled_entities(
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test entities enabled by default."""
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
entity_registry: EntityRegistry = er.async_get(hass)
client = await hass_client_no_auth()

View File

@ -44,7 +44,7 @@ def zigpy_device(zigpy_device_mock):
"""Device tracker zigpy device."""
endpoints = {
1: {
SIG_EP_INPUT: [security.IasAce.cluster_id],
SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL,
SIG_EP_PROFILE: zha.PROFILE_ID,
@ -93,6 +93,22 @@ async def test_diagnostics_for_device(
) -> None:
"""Test diagnostics for device."""
zha_device: ZHADevice = await zha_device_joined(zigpy_device)
# add unknown unsupported attribute with id and name
zha_device.device.endpoints[1].in_clusters[
security.IasAce.cluster_id
].unsupported_attributes.update({0x1000, "unknown_attribute_name"})
# add known unsupported attributes with id and name
zha_device.device.endpoints[1].in_clusters[
security.IasZone.cluster_id
].unsupported_attributes.update(
{
security.IasZone.AttributeDefs.num_zone_sensitivity_levels_supported.id,
security.IasZone.AttributeDefs.current_zone_sensitivity_level.name,
}
)
dev_reg = async_get(hass)
device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))})
assert device