mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
2023.10.1 (#101547)
This commit is contained in:
commit
a6edfa85b1
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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.",
|
||||
|
@ -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}"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)},
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
DOMAIN = "waze_travel_time"
|
||||
SEMAPHORE = "semaphore"
|
||||
|
||||
CONF_DESTINATION = "destination"
|
||||
CONF_ORIGIN = "origin"
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -89,6 +89,9 @@
|
||||
"maximum_heart_rate": {
|
||||
"name": "Maximum heart rate"
|
||||
},
|
||||
"minimum_heart_rate": {
|
||||
"name": "Minimum heart rate"
|
||||
},
|
||||
"light_sleep": {
|
||||
"name": "Light sleep"
|
||||
},
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.115.1"]
|
||||
"requirements": ["zeroconf==0.115.2"]
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'>,
|
||||
}),
|
||||
|
@ -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': '',
|
||||
})
|
||||
# ---
|
||||
|
@ -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):
|
||||
|
@ -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"}
|
||||
|
21
tests/components/color_extractor/conftest.py
Normal file
21
tests/components/color_extractor/conftest.py
Normal 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()
|
17
tests/components/color_extractor/test_init.py
Normal file
17
tests/components/color_extractor/test_init.py
Normal 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
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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": [
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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"),
|
||||
[
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user