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.core import Context, HomeAssistant
from homeassistant.helpers.typing import ConfigType 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 .error import PipelineNotFound
from .pipeline import ( from .pipeline import (
AudioSettings, AudioSettings,
@ -45,7 +45,9 @@ __all__ = (
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{vol.Optional("debug_recording_dir"): str}, {
vol.Optional(CONF_DEBUG_RECORDING_DIR): str,
},
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,

View File

@ -2,3 +2,12 @@
DOMAIN = "assist_pipeline" DOMAIN = "assist_pipeline"
DATA_CONFIG = f"{DOMAIN}.config" 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 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 ( from .error import (
IntentRecognitionError, IntentRecognitionError,
PipelineError, PipelineError,
@ -399,6 +405,9 @@ class WakeWordSettings:
audio_seconds_to_buffer: float = 0 audio_seconds_to_buffer: float = 0
"""Seconds of audio to buffer before detection and forward to STT.""" """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) @dataclass(frozen=True)
class AudioSettings: 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 # Remove language since it doesn't apply to wake words yet
metadata_dict.pop("language", None) metadata_dict.pop("language", None)
@ -612,6 +623,7 @@ class PipelineRun:
{ {
"entity_id": self.wake_word_entity_id, "entity_id": self.wake_word_entity_id,
"metadata": metadata_dict, "metadata": metadata_dict,
"timeout": wake_word_settings.timeout or 0,
}, },
) )
) )
@ -619,8 +631,6 @@ class PipelineRun:
if self.debug_recording_queue is not None: if self.debug_recording_queue is not None:
self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_entity_id}") 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 wake_word_vad: VoiceActivityTimeout | None = None
if (wake_word_settings.timeout is not None) and ( if (wake_word_settings.timeout is not None) and (
wake_word_settings.timeout > 0 wake_word_settings.timeout > 0
@ -670,6 +680,17 @@ class PipelineRun:
if result is None: if result is None:
wake_word_output: dict[str, Any] = {} wake_word_output: dict[str, Any] = {}
else: 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: if result.queued_audio:
# Add audio that was pending at detection. # Add audio that was pending at detection.
# #
@ -1032,7 +1053,7 @@ class PipelineRun:
# Directory to save audio for each pipeline run. # Directory to save audio for each pipeline run.
# Configured in YAML for assist_pipeline. # Configured in YAML for assist_pipeline.
if debug_recording_dir := self.hass.data[DATA_CONFIG].get( if debug_recording_dir := self.hass.data[DATA_CONFIG].get(
"debug_recording_dir" CONF_DEBUG_RECORDING_DIR
): ):
if device_id is None: if device_id is None:
# <debug_recording_dir>/<pipeline.name>/<run.id> # <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.helpers import config_validation as cv
from homeassistant.util import language as language_util 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 .error import PipelineNotFound
from .pipeline import ( from .pipeline import (
AudioSettings, AudioSettings,
@ -30,9 +30,6 @@ from .pipeline import (
async_get_pipeline, async_get_pipeline,
) )
DEFAULT_TIMEOUT = 60 * 5 # seconds
DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -117,7 +114,7 @@ async def websocket_run(
) )
return return
timeout = msg.get("timeout", DEFAULT_TIMEOUT) timeout = msg.get("timeout", DEFAULT_PIPELINE_TIMEOUT)
start_stage = PipelineStage(msg["start_stage"]) start_stage = PipelineStage(msg["start_stage"])
end_stage = PipelineStage(msg["end_stage"]) end_stage = PipelineStage(msg["end_stage"])
handler_id: int | None = None handler_id: int | None = None

View File

@ -286,6 +286,13 @@
"on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" "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": { "update": {
"name": "Update", "name": "Update",
"state": { "state": {

View File

@ -19,6 +19,6 @@
"bluetooth-adapters==0.16.1", "bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3", "bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.12.0", "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 @callback
def update(_: datetime.datetime) -> None: 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) _LOGGER.debug("Running %s update", self.entity_id)
self._async_write_ha_state() self.async_write_ha_state()
if now < event.start_datetime_local: if now < event.start_datetime_local:
self._alarm_unsubs.append( 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__) _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 # Extend the existing light.turn_on service schema
SERVICE_SCHEMA = vol.All( SERVICE_SCHEMA = vol.All(
@ -62,11 +65,12 @@ def _get_color(file_handler) -> tuple:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Color extractor component.""" """Set up the Color extractor component."""
hass.async_create_task( if DOMAIN in config:
hass.config_entries.flow.async_init( hass.async_create_task(
DOMAIN, context={"source": SOURCE_IMPORT}, data={} hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
)
) )
)
return True return True

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["env_canada"], "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", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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", "iot_class": "local_push",
"loggers": ["pyhap"], "loggers": ["pyhap"],
"requirements": [ "requirements": [
"HAP-python==4.7.1", "HAP-python==4.8.0",
"fnv-hash-fast==0.4.1", "fnv-hash-fast==0.4.1",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1" "base36==0.1.1"

View File

@ -7,7 +7,7 @@
"dependencies": ["webhook"], "dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/loqed", "documentation": "https://www.home-assistant.io/integrations/loqed",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["loqedAPI==2.1.7"], "requirements": ["loqedAPI==2.1.8"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "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.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.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.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)), 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" f"{name}: Size of structure is {size} bytes but `{CONF_COUNT}: {count}` is {bytecount} bytes"
) )
else: 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: if slave_count:
structure = ( structure = (
f">{slave_count + 1}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" 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: def valid_config(config: ConfigType) -> ConfigType:
"""Check if there is a state topic or json_attributes_topic.""" """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: 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}" f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}"
) )
return config return config

View File

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

View File

@ -277,7 +277,8 @@ class MinutPointEntity(Entity):
sw_version=device["firmware"]["installed"], sw_version=device["firmware"]["installed"],
via_device=(DOMAIN, device["home"]), 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): def __str__(self):
"""Return string representation of device.""" """Return string representation of device."""

View File

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

View File

@ -51,7 +51,7 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity
) -> None: ) -> None:
"""Initialize the Rain Bird sensor.""" """Initialize the Rain Bird sensor."""
super().__init__(coordinator) 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_unique_id = f"{coordinator.unique_id}-rain-delay"
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
else: else:

View File

@ -52,7 +52,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity)
"""Initialize the Rain Bird sensor.""" """Initialize the Rain Bird sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description 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_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
else: else:

View File

@ -65,17 +65,18 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
"""Initialize a Rain Bird Switch Device.""" """Initialize a Rain Bird Switch Device."""
super().__init__(coordinator) super().__init__(coordinator)
self._zone = zone 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}" self._attr_unique_id = f"{coordinator.unique_id}-{zone}"
device_name = f"{MANUFACTURER} Sprinkler {zone}" device_name = f"{MANUFACTURER} Sprinkler {zone}"
if imported_name: if imported_name:
self._attr_name = imported_name self._attr_name = imported_name
self._attr_has_entity_name = False self._attr_has_entity_name = False
else: 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._attr_has_entity_name = True
self._duration_minutes = duration_minutes 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( self._attr_device_info = DeviceInfo(
name=device_name, name=device_name,
identifiers={(DOMAIN, self._attr_unique_id)}, 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 for metadata_id in missing_metadata_ids
if ( if (
latest_id := cache_latest_short_term_statistic_id_for_metadata_id( 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 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 # We just inserted new short term statistics, so we need to update the
# ShortTermStatisticsRunCache with the latest id for the metadata_id # ShortTermStatisticsRunCache with the latest id for the metadata_id
run_cache = get_short_term_statistics_run_cache(instance.hass) 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( cache_latest_short_term_statistic_id_for_metadata_id(
run_cache, session, metadata_id run_cache, session, metadata_id, orm_rows=True
) )
return True return True
@ -2326,7 +2338,10 @@ def get_short_term_statistics_run_cache(
def cache_latest_short_term_statistic_id_for_metadata_id( 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: ) -> int | None:
"""Cache the latest short term statistic for a given metadata_id. """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( execute_stmt_lambda_element(
session, session,
_find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id), _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 id_: int = latest[0].id

View File

@ -139,6 +139,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False, 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( "insulation_residual_current": SensorEntityDescription(
key="insulation_residual_current", key="insulation_residual_current",
name="Insulation Residual Current", name="Insulation Residual Current",
@ -147,6 +154,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False, 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( "grid_power": SensorEntityDescription(
key="grid_power", key="grid_power",
name="Grid Power", name="Grid Power",
@ -479,6 +493,30 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY, 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( "optimizer_power": SensorEntityDescription(
key="optimizer_power", key="optimizer_power",
name="Optimizer Power", name="Optimizer Power",

View File

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

View File

@ -1,13 +1,19 @@
"""The waze_travel_time component.""" """The waze_travel_time component."""
import asyncio
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN, SEMAPHORE
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Load the saved entities.""" """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) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True return True

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pywaze", "homeassistant.helpers.location"], "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, DEFAULT_NAME,
DOMAIN, DOMAIN,
IMPERIAL_UNITS, IMPERIAL_UNITS,
SEMAPHORE,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -51,7 +52,7 @@ SCAN_INTERVAL = timedelta(minutes=5)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
MS_BETWEEN_API_CALLS = 0.5 SECONDS_BETWEEN_API_CALLS = 0.5
async def async_setup_entry( async def async_setup_entry(
@ -148,8 +149,12 @@ class WazeTravelTime(SensorEntity):
_LOGGER.debug("Fetching Route for %s", self._attr_name) _LOGGER.debug("Fetching Route for %s", self._attr_name)
self._waze_data.origin = find_coordinates(self.hass, self._origin) self._waze_data.origin = find_coordinates(self.hass, self._origin)
self._waze_data.destination = find_coordinates(self.hass, self._destination) self._waze_data.destination = find_coordinates(self.hass, self._destination)
await self._waze_data.async_update() await self.hass.data[DOMAIN][SEMAPHORE].acquire()
await asyncio.sleep(MS_BETWEEN_API_CALLS) try:
await self._waze_data.async_update()
await asyncio.sleep(SECONDS_BETWEEN_API_CALLS)
finally:
self.hass.data[DOMAIN][SEMAPHORE].release()
class WazeTravelTimeData: class WazeTravelTimeData:

View File

@ -245,7 +245,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
translation_key="wind_gust", translation_key="wind_gust",
icon="mdi:weather-windy", icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED, 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, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2, suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude, raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
@ -255,7 +255,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
translation_key="wind_lull", translation_key="wind_lull",
icon="mdi:weather-windy", icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED, 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, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2, suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude, raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
@ -265,17 +265,17 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
icon="mdi:weather-windy", icon="mdi:weather-windy",
event_subscriptions=[EVENT_RAPID_WIND, EVENT_OBSERVATION], 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, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2, suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude, raw_data_conv_fn=lambda raw_data: raw_data.magnitude,
), ),
WeatherFlowSensorEntityDescription( WeatherFlowSensorEntityDescription(
key="wind_speed_average", key="wind_average",
translation_key="wind_speed_average", translation_key="wind_speed_average",
icon="mdi:weather-windy", icon="mdi:weather-windy",
device_class=SensorDeviceClass.WIND_SPEED, 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, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2, suggested_display_precision=2,
raw_data_conv_fn=lambda raw_data: raw_data.magnitude, 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 __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import contextlib
from typing import Any from typing import Any
from aiohttp.hdrs import METH_HEAD, METH_POST 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 import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue 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 homeassistant.helpers.typing import ConfigType
from .api import ConfigEntryWithingsApi from .api import ConfigEntryWithingsApi
@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
webhook_name = "Withings" webhook_name = "Withings"
if entry.title != DEFAULT_TITLE: if entry.title != DEFAULT_TITLE:
webhook_name = " ".join([DEFAULT_TITLE, entry.title]) webhook_name = f"{DEFAULT_TITLE} {entry.title}"
webhook_register( webhook_register(
hass, hass,
@ -182,14 +182,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED: if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED:
await unregister_webhook(None) 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_active_subscription(hass):
if cloud.async_is_connected(hass): if cloud.async_is_connected(hass):
await register_webhook(None) entry.async_on_unload(async_call_later(hass, 1, register_webhook))
cloud.async_listen_connection_change(hass, manage_cloudhook) entry.async_on_unload(
cloud.async_listen_connection_change(hass, manage_cloudhook)
)
else: 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener)) 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: async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
"""Generate the full URL for a webhook_id.""" """Generate the full URL for a webhook_id."""
if CONF_CLOUDHOOK_URL not in entry.data: if CONF_CLOUDHOOK_URL not in entry.data:
webhook_url = await cloud.async_create_cloudhook( webhook_id = entry.data[CONF_WEBHOOK_ID]
hass, 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} data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
hass.config_entries.async_update_entry(entry, data=data) hass.config_entries.async_update_entry(entry, data=data)
return webhook_url return webhook_url

View File

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

View File

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

View File

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

View File

@ -8,5 +8,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["zeroconf"], "loggers": ["zeroconf"],
"quality_scale": "internal", "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: def get_cluster_attr_data(cluster: Cluster) -> dict:
"""Return cluster attribute data.""" """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 { return {
ATTRIBUTES: { ATTRIBUTES: {
f"0x{attr_id:04x}": { 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() for attr_id, attr_def in cluster.attributes.items()
if (attr_value := cluster.get(attr_def.name)) is not None if (attr_value := cluster.get(attr_def.name)) is not None
}, },
UNSUPPORTED_ATTRIBUTES: { UNSUPPORTED_ATTRIBUTES: 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
},
} }

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) 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 certifi>=2021.5.30
ciso8601==2.3.0 ciso8601==2.3.0
cryptography==41.0.4 cryptography==41.0.4
dbus-fast==2.11.0 dbus-fast==2.11.1
fnv-hash-fast==0.4.1 fnv-hash-fast==0.4.1
ha-av==10.1.1 ha-av==10.1.1
hass-nabucasa==0.71.0 hass-nabucasa==0.71.0
hassil==1.2.5 hassil==1.2.5
home-assistant-bluetooth==1.10.3 home-assistant-bluetooth==1.10.3
home-assistant-frontend==20231002.0 home-assistant-frontend==20231005.0
home-assistant-intents==2023.10.2 home-assistant-intents==2023.10.2
httpx==0.24.1 httpx==0.24.1
ifaddr==0.2.0 ifaddr==0.2.0
@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1 voluptuous==0.13.1
webrtc-noise-gain==1.2.3 webrtc-noise-gain==1.2.3
yarl==1.9.2 yarl==1.9.2
zeroconf==0.115.1 zeroconf==0.115.2
# Constrain pycryptodome to avoid vulnerability # Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238 # see https://github.com/home-assistant/core/pull/16238

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.10.0" version = "2023.10.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -437,7 +437,7 @@ filterwarnings = [
# -- design choice 3rd party # -- design choice 3rd party
# https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19 # 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", "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", "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 # https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",

View File

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

View File

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

View File

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

View File

@ -373,6 +373,7 @@
'format': 'wav', 'format': 'wav',
'sample_rate': 16000, 'sample_rate': 16000,
}), }),
'timeout': 0,
}) })
# --- # ---
# name: test_audio_pipeline_with_wake_word_no_timeout.2 # name: test_audio_pipeline_with_wake_word_no_timeout.2
@ -474,6 +475,7 @@
'format': 'wav', 'format': 'wav',
'sample_rate': 16000, 'sample_rate': 16000,
}), }),
'timeout': 1,
}) })
# --- # ---
# name: test_audio_pipeline_with_wake_word_timeout.2 # name: test_audio_pipeline_with_wake_word_timeout.2
@ -655,3 +657,63 @@
# name: test_tts_failed.2 # name: test_tts_failed.2
None 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 syrupy.assertion import SnapshotAssertion
from homeassistant.components import assist_pipeline, stt 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.core import Context, HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -395,8 +399,8 @@ async def test_pipeline_save_audio(
temp_dir = Path(temp_dir_str) temp_dir = Path(temp_dir_str)
assert await async_setup_component( assert await async_setup_component(
hass, hass,
"assist_pipeline", DOMAIN,
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
) )
pipeline = assist_pipeline.async_get_pipeline(hass) 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) temp_dir = Path(temp_dir_str)
assert await async_setup_component( assert await async_setup_component(
hass, hass,
"assist_pipeline", DOMAIN,
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
) )
def event_callback(event: assist_pipeline.PipelineEvent): 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) temp_dir = Path(temp_dir_str)
assert await async_setup_component( assert await async_setup_component(
hass, hass,
"assist_pipeline", DOMAIN,
{"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
) )
def event_callback(event: assist_pipeline.PipelineEvent): 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.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .conftest import MockWakeWordEntity
from tests.typing import WebSocketGenerator from tests.typing import WebSocketGenerator
@ -266,7 +268,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout(
events.append(msg["event"]) events.append(msg["event"])
# "audio" # "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() msg = await client.receive_json()
assert msg["event"]["type"] == "wake_word-end" assert msg["event"]["type"] == "wake_word-end"
@ -1805,3 +1807,84 @@ async def test_audio_pipeline_with_enhancements(
msg = await client.receive_json() msg = await client.receive_json()
assert msg["success"] assert msg["success"]
assert msg["result"] == {"events": events} 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.""" """Tests for color_extractor component service calls."""
import base64 import base64
import io import io
from typing import Any
from unittest.mock import Mock, mock_open, patch from unittest.mock import Mock, mock_open, patch
import aiohttp import aiohttp
@ -92,15 +93,8 @@ async def setup_light(hass: HomeAssistant):
assert state.state == STATE_OFF 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.""" """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 # Validate pre service call
state = hass.states.get(LIGHT_ENTITY) 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 assert state.state == STATE_OFF
async def _async_load_color_extractor_url(hass, service_data): async def _async_execute_service(hass: HomeAssistant, service_data: dict[str, Any]):
# Load our color_extractor component
await async_setup_component(
hass,
DOMAIN,
{},
)
await hass.async_block_till_done()
# Validate pre service call # Validate pre service call
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
assert state assert state
@ -145,7 +131,7 @@ async def _async_load_color_extractor_url(hass, service_data):
async def test_url_success( async def test_url_success(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None: ) -> None:
"""Test that a successful image GET translate to light RGB.""" """Test that a successful image GET translate to light RGB."""
service_data = { service_data = {
@ -158,13 +144,15 @@ async def test_url_success(
# Mock the HTTP Response with a base64 encoded 1x1 pixel # Mock the HTTP Response with a base64 encoded 1x1 pixel
aioclient_mock.get( aioclient_mock.get(
url=service_data[ATTR_URL], 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 # Allow access to this URL using the proper mechanism
hass.config.allowlist_external_urls.add("http://example.com/images/") 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) state = hass.states.get(LIGHT_ENTITY)
assert state assert state
@ -180,7 +168,7 @@ async def test_url_success(
async def test_url_not_allowed( async def test_url_not_allowed(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None: ) -> None:
"""Test that a not allowed external URL fails to turn light on.""" """Test that a not allowed external URL fails to turn light on."""
service_data = { service_data = {
@ -188,7 +176,7 @@ async def test_url_not_allowed(
ATTR_ENTITY_ID: LIGHT_ENTITY, 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 # Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
@ -197,7 +185,7 @@ async def test_url_not_allowed(
async def test_url_exception( async def test_url_exception(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None: ) -> None:
"""Test that a HTTPError fails to turn light on.""" """Test that a HTTPError fails to turn light on."""
service_data = { service_data = {
@ -211,7 +199,7 @@ async def test_url_exception(
# Mock the HTTP Response with an HTTPError # Mock the HTTP Response with an HTTPError
aioclient_mock.get(url=service_data[ATTR_URL], exc=aiohttp.ClientError) 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 # Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
@ -220,7 +208,7 @@ async def test_url_exception(
async def test_url_error( async def test_url_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration
) -> None: ) -> None:
"""Test that a HTTP Error (non 200) doesn't turn light on.""" """Test that a HTTP Error (non 200) doesn't turn light on."""
service_data = { service_data = {
@ -234,7 +222,7 @@ async def test_url_error(
# Mock the HTTP Response with a 400 Bad Request error # Mock the HTTP Response with a 400 Bad Request error
aioclient_mock.get(url=service_data[ATTR_URL], status=400) 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 # Light has not been modified due to failure
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
@ -244,7 +232,11 @@ async def test_url_error(
@patch( @patch(
"builtins.open", "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, create=True,
) )
def _get_file_mock(file_path): 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.path.isfile", Mock(return_value=True))
@patch("os.access", 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.""" """Test that the file only service reads a file and translates to light RGB."""
service_data = { service_data = {
ATTR_PATH: "/opt/image.png", 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 # Add our /opt/ path to the allowed list of paths
hass.config.allowlist_external_dirs.add("/opt/") hass.config.allowlist_external_dirs.add("/opt/")
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Verify pre service check # Verify pre service check
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
assert state assert state
@ -303,7 +292,7 @@ async def test_file(hass: HomeAssistant) -> None:
@patch("os.path.isfile", Mock(return_value=True)) @patch("os.path.isfile", Mock(return_value=True))
@patch("os.access", 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.""" """Test that the file only service fails to read an image in a dir not explicitly allowed."""
service_data = { service_data = {
ATTR_PATH: "/path/to/a/dir/not/allowed/image.png", 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, ATTR_BRIGHTNESS_PCT: 100,
} }
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Verify pre service check # Verify pre service check
state = hass.states.get(LIGHT_ENTITY) state = hass.states.get(LIGHT_ENTITY)
assert state assert state

View File

@ -122,6 +122,28 @@ async def test_invalid_json(
assert not mock_dispatcher_send.called 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( async def test_only_valid_components(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_mock_entry: MqttMockHAClientGenerator,

View File

@ -19,6 +19,8 @@ def mock_nextbus_lists(mock_nextbus: MagicMock) -> MagicMock:
"stop": [ "stop": [
{"tag": "5650", "title": "Market St & 7th St"}, {"tag": "5650", "title": "Market St & 7th St"},
{"tag": "5651", "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": [ "direction": [
{ {

View File

@ -17,6 +17,7 @@ from .conftest import (
PASSWORD, PASSWORD,
RAIN_DELAY_OFF, RAIN_DELAY_OFF,
RAIN_SENSOR_OFF, RAIN_SENSOR_OFF,
SERIAL_NUMBER,
ZONE_3_ON_RESPONSE, ZONE_3_ON_RESPONSE,
ZONE_5_ON_RESPONSE, ZONE_5_ON_RESPONSE,
ZONE_OFF_RESPONSE, ZONE_OFF_RESPONSE,
@ -286,7 +287,7 @@ async def test_switch_error(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("config_entry_unique_id"), ("config_entry_unique_id"),
[ [
None, (None),
], ],
) )
async def test_no_unique_id( 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") entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3")
assert entity_entry is None 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 ( from homeassistant.components.recorder.statistics import (
async_add_external_statistics, async_add_external_statistics,
get_last_statistics, get_last_statistics,
get_latest_short_term_statistics,
get_metadata, get_metadata,
get_short_term_statistics_run_cache, get_short_term_statistics_run_cache,
list_statistic_ids, list_statistic_ids,
@ -635,6 +636,22 @@ async def test_statistic_during_period(
"change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"])
* 1000, * 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)) @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.""" """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.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower
from homeassistant.core import HomeAssistant 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") state = hass.states.get("sensor.sma_device_grid_power")
assert state assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT 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", status="Running",
camera_type="Road", 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" 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( @pytest.mark.parametrize(
("side_effect", "error_key", "base_error"), ("side_effect", "error_key", "base_error"),
[ [

View File

@ -1,15 +1,17 @@
"""Tests for the withings component.""" """Tests for the withings component."""
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from typing import Any from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.webhook import async_generate_url from homeassistant.components.webhook import async_generate_url
from homeassistant.config import async_process_ha_core_config from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
@dataclass @dataclass
@ -53,3 +55,12 @@ async def setup_integration(
) )
await hass.config_entries.async_setup(config_entry.entry_id) 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 # name: test_all_entities.21
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'henk Fat mass', 'friendly_name': 'henk Maximum heart rate',
'icon': 'mdi:heart-pulse', 'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm', 'unit_of_measurement': 'bpm',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_fat_mass_2', 'entity_id': 'sensor.henk_maximum_heart_rate',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '165.0', 'state': '165.0',
@ -226,13 +226,13 @@
# name: test_all_entities.22 # name: test_all_entities.22
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'henk Maximum heart rate', 'friendly_name': 'henk Minimum heart rate',
'icon': 'mdi:heart-pulse', 'icon': 'mdi:heart-pulse',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'bpm', 'unit_of_measurement': 'bpm',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.henk_maximum_heart_rate', 'entity_id': 'sensor.henk_minimum_heart_rate',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '166.0', 'state': '166.0',

View File

@ -2,13 +2,14 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from aiohttp.client_exceptions import ClientResponseError from aiohttp.client_exceptions import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from withings_api.common import NotifyAppli from withings_api.common import NotifyAppli
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant 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 .conftest import USER_ID, WEBHOOK_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -20,9 +21,11 @@ async def test_binary_sensor(
withings: AsyncMock, withings: AsyncMock,
webhook_config_entry: MockConfigEntry, webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test binary sensor.""" """Test binary sensor."""
await setup_integration(hass, webhook_config_entry) await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth() 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.webhook import async_generate_url
from homeassistant.components.withings import CONFIG_SCHEMA, async_setup from homeassistant.components.withings import CONFIG_SCHEMA, async_setup
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
from homeassistant.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID
CONF_CLIENT_ID, from homeassistant.core import HomeAssistant
CONF_CLIENT_SECRET,
CONF_WEBHOOK_ID,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.util import dt as dt_util 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 .conftest import USER_ID, WEBHOOK_ID
from tests.common import ( from tests.common import (
@ -197,9 +192,12 @@ async def test_webhooks_request_data(
withings: AsyncMock, withings: AsyncMock,
webhook_config_entry: MockConfigEntry, webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test calling a webhook requests data.""" """Test calling a webhook requests data."""
await setup_integration(hass, webhook_config_entry) await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth() client = await hass_client_no_auth()
assert withings.async_measure_get_meas.call_count == 1 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 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( @pytest.mark.parametrize(
"error", "error",
[ [
@ -395,7 +364,10 @@ async def test_removing_entry_with_cloud_unavailable(
async def test_setup_with_cloud( 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: ) -> None:
"""Test if set up with active cloud subscription.""" """Test if set up with active cloud subscription."""
await mock_cloud(hass) await mock_cloud(hass)
@ -418,9 +390,12 @@ async def test_setup_with_cloud(
"homeassistant.components.withings.webhook_generate_url" "homeassistant.components.withings.webhook_generate_url"
): ):
await setup_integration(hass, webhook_config_entry) 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_active_subscription() is True
assert hass.components.cloud.async_is_connected() is True assert hass.components.cloud.async_is_connected() is True
fake_create_cloudhook.assert_called_once() fake_create_cloudhook.assert_called_once()
fake_delete_cloudhook.assert_called_once()
assert ( assert (
hass.config_entries.async_entries("withings")[0].data["cloudhook_url"] 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"): for config_entry in hass.config_entries.async_entries("withings"):
await hass.config_entries.async_remove(config_entry.entry_id) 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() await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN) assert not hass.config_entries.async_entries(DOMAIN)
@ -443,6 +418,7 @@ async def test_setup_without_https(
webhook_config_entry: MockConfigEntry, webhook_config_entry: MockConfigEntry,
withings: AsyncMock, withings: AsyncMock,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test if set up with cloud link and without https.""" """Test if set up with cloud link and without https."""
hass.config.components.add("cloud") hass.config.components.add("cloud")
@ -456,6 +432,7 @@ async def test_setup_without_https(
) as mock_async_generate_url: ) as mock_async_generate_url:
mock_async_generate_url.return_value = "http://example.com" mock_async_generate_url.return_value = "http://example.com"
await setup_integration(hass, webhook_config_entry) await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_async_generate_url.assert_called_once() mock_async_generate_url.assert_called_once()
@ -491,6 +468,7 @@ async def test_cloud_disconnect(
"homeassistant.components.withings.webhook_generate_url" "homeassistant.components.withings.webhook_generate_url"
): ):
await setup_integration(hass, webhook_config_entry) 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_active_subscription() is True
assert hass.components.cloud.async_is_connected() is True assert hass.components.cloud.async_is_connected() is True
@ -536,9 +514,11 @@ async def test_webhook_post(
body: dict[str, Any], body: dict[str, Any],
expected_code: int, expected_code: int,
current_request_with_host: None, current_request_with_host: None,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test webhook callback.""" """Test webhook callback."""
await setup_integration(hass, webhook_config_entry) await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
client = await hass_client_no_auth() client = await hass_client_no_auth()
webhook_url = async_generate_url(hass, WEBHOOK_ID) 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 import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry 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 .conftest import USER_ID, WEBHOOK_ID
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -96,9 +96,11 @@ async def test_sensor_default_enabled_entities(
withings: AsyncMock, withings: AsyncMock,
webhook_config_entry: MockConfigEntry, webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test entities enabled by default.""" """Test entities enabled by default."""
await setup_integration(hass, webhook_config_entry) await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
entity_registry: EntityRegistry = er.async_get(hass) entity_registry: EntityRegistry = er.async_get(hass)
client = await hass_client_no_auth() client = await hass_client_no_auth()

View File

@ -44,7 +44,7 @@ def zigpy_device(zigpy_device_mock):
"""Device tracker zigpy device.""" """Device tracker zigpy device."""
endpoints = { endpoints = {
1: { 1: {
SIG_EP_INPUT: [security.IasAce.cluster_id], SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id],
SIG_EP_OUTPUT: [], SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL,
SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_PROFILE: zha.PROFILE_ID,
@ -93,6 +93,22 @@ async def test_diagnostics_for_device(
) -> None: ) -> None:
"""Test diagnostics for device.""" """Test diagnostics for device."""
zha_device: ZHADevice = await zha_device_joined(zigpy_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) dev_reg = async_get(hass)
device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))})
assert device assert device