This commit is contained in:
Franck Nijhof 2023-12-08 22:13:24 +01:00 committed by GitHub
commit 9b10af612a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 861 additions and 162 deletions

View File

@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field
from enum import StrEnum from enum import StrEnum
import logging import logging
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Empty, Queue
from threading import Thread from threading import Thread
import time import time
from typing import TYPE_CHECKING, Any, Final, cast from typing import TYPE_CHECKING, Any, Final, cast
@ -1010,8 +1010,8 @@ class PipelineRun:
self.tts_engine = engine self.tts_engine = engine
self.tts_options = tts_options self.tts_options = tts_options
async def text_to_speech(self, tts_input: str) -> str: async def text_to_speech(self, tts_input: str) -> None:
"""Run text-to-speech portion of pipeline. Returns URL of TTS audio.""" """Run text-to-speech portion of pipeline."""
self.process_event( self.process_event(
PipelineEvent( PipelineEvent(
PipelineEventType.TTS_START, PipelineEventType.TTS_START,
@ -1058,8 +1058,6 @@ class PipelineRun:
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
) )
return tts_media.url
def _capture_chunk(self, audio_bytes: bytes | None) -> None: def _capture_chunk(self, audio_bytes: bytes | None) -> None:
"""Forward audio chunk to various capturing mechanisms.""" """Forward audio chunk to various capturing mechanisms."""
if self.debug_recording_queue is not None: if self.debug_recording_queue is not None:
@ -1246,6 +1244,8 @@ def _pipeline_debug_recording_thread_proc(
# Chunk of 16-bit mono audio at 16Khz # Chunk of 16-bit mono audio at 16Khz
if wav_writer is not None: if wav_writer is not None:
wav_writer.writeframes(message) wav_writer.writeframes(message)
except Empty:
pass # occurs when pipeline has unexpected error
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
_LOGGER.exception("Unexpected error in debug recording thread") _LOGGER.exception("Unexpected error in debug recording thread")
finally: finally:

View File

@ -55,7 +55,9 @@ _LOGGER = logging.getLogger(__name__)
_AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge") _AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge")
_FuncType = Callable[[_AsusWrtBridgeT], Awaitable[list[Any] | dict[str, Any]]] _FuncType = Callable[
[_AsusWrtBridgeT], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]
]
_ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]] _ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]]
@ -81,7 +83,7 @@ def handle_errors_and_zip(
if isinstance(data, dict): if isinstance(data, dict):
return dict(zip(keys, list(data.values()))) return dict(zip(keys, list(data.values())))
if not isinstance(data, list): if not isinstance(data, (list, tuple)):
raise UpdateFailed("Received invalid data type") raise UpdateFailed("Received invalid data type")
return dict(zip(keys, data)) return dict(zip(keys, data))

View File

@ -649,7 +649,7 @@ class DefaultAgent(AbstractConversationAgent):
if device_area is None: if device_area is None:
return None return None
return {"area": device_area.name} return {"area": device_area.id}
def _get_error_text( def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents | None self, response_type: ResponseType, lang_intents: LanguageIntents | None

View File

@ -67,7 +67,7 @@ DECONZ_TO_COLOR_MODE = {
LightColorMode.XY: ColorMode.XY, LightColorMode.XY: ColorMode.XY,
} }
TS0601_EFFECTS = [ XMAS_LIGHT_EFFECTS = [
"carnival", "carnival",
"collide", "collide",
"fading", "fading",
@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
if device.effect is not None: if device.effect is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_effect_list = [EFFECT_COLORLOOP] self._attr_effect_list = [EFFECT_COLORLOOP]
if device.model_id == "TS0601": if device.model_id in ("HG06467", "TS0601"):
self._attr_effect_list += TS0601_EFFECTS self._attr_effect_list = XMAS_LIGHT_EFFECTS
@property @property
def color_mode(self) -> str | None: def color_mode(self) -> str | None:

View File

@ -183,6 +183,7 @@ async def async_setup_entry(
for description in sensors for description in sensors
for value_key in {description.key, *description.alternative_keys} for value_key in {description.key, *description.alternative_keys}
if description.value_fn(coordinator.data, value_key, description.scale) if description.value_fn(coordinator.data, value_key, description.scale)
is not None
) )
async_add_entities(entities) async_add_entities(entities)

View File

@ -317,6 +317,11 @@ class EnergyCostSensor(SensorEntity):
try: try:
energy_price = float(energy_price_state.state) energy_price = float(energy_price_state.state)
except ValueError: except ValueError:
if self._last_energy_sensor_state is None:
# Initialize as it's the first time all required entities except
# price are in place. This means that the cost will update the first
# time the energy is updated after the price entity is in place.
self._reset(energy_state)
return return
energy_price_unit: str | None = energy_price_state.attributes.get( energy_price_unit: str | None = energy_price_state.attributes.get(

View File

@ -38,11 +38,9 @@ async def async_setup_entry(
FritzboxLight( FritzboxLight(
coordinator, coordinator,
ain, ain,
device.get_colors(),
device.get_color_temps(),
) )
for ain in coordinator.new_devices for ain in coordinator.new_devices
if (device := coordinator.data.devices[ain]).has_lightbulb if (coordinator.data.devices[ain]).has_lightbulb
) )
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@ -57,27 +55,10 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
self, self,
coordinator: FritzboxDataUpdateCoordinator, coordinator: FritzboxDataUpdateCoordinator,
ain: str, ain: str,
supported_colors: dict,
supported_color_temps: list[int],
) -> None: ) -> None:
"""Initialize the FritzboxLight entity.""" """Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None) super().__init__(coordinator, ain, None)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
self._supported_hs: dict[int, list[int]] = {} self._supported_hs: dict[int, list[int]] = {}
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -173,3 +154,28 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off.""" """Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off) await self.hass.async_add_executor_job(self.data.set_state_off)
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
async def async_added_to_hass(self) -> None:
"""Get light attributes from device after entity is added to hass."""
await super().async_added_to_hass()
supported_colors = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_colors
)
supported_color_temps = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_color_temps
)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]

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==20231206.0"] "requirements": ["home-assistant-frontend==20231208.2"]
} }

View File

@ -38,6 +38,7 @@ DEFAULT_EXPOSED_DOMAINS = {
"scene", "scene",
"script", "script",
"switch", "switch",
"todo",
"vacuum", "vacuum",
"water_heater", "water_heater",
} }

View File

@ -1,12 +1,61 @@
"""The Homewizard integration.""" """The Homewizard integration."""
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from .const import DOMAIN, PLATFORMS from .const import DOMAIN, LOGGER, PLATFORMS
from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator
async def _async_migrate_entries(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Migrate old entry.
The HWE-SKT had no total_power_*_kwh in 2023.11, in 2023.12 it does.
But simultaneously, the total_power_*_t1_kwh was removed for HWE-SKT.
This migration migrates the old unique_id to the new one, if possible.
Migration can be removed after 2024.6
"""
entity_registry = er.async_get(hass)
@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
replacements = {
"total_power_import_t1_kwh": "total_power_import_kwh",
"total_power_export_t1_kwh": "total_power_export_kwh",
}
for old_id, new_id in replacements.items():
if entry.unique_id.endswith(old_id):
new_unique_id = entry.unique_id.replace(old_id, new_id)
if existing_entity_id := entity_registry.async_get_entity_id(
entry.domain, entry.platform, new_unique_id
):
LOGGER.debug(
"Cannot migrate to unique_id '%s', already exists for '%s'",
new_unique_id,
existing_entity_id,
)
return None
LOGGER.debug(
"Migrating entity '%s' unique_id from '%s' to '%s'",
entry.entity_id,
entry.unique_id,
new_unique_id,
)
return {
"new_unique_id": new_unique_id,
}
return None
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homewizard from a config entry.""" """Set up Homewizard from a config entry."""
coordinator = Coordinator(hass) coordinator = Coordinator(hass)
@ -21,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise raise
await _async_migrate_entries(hass, entry)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
# Abort reauth config flow if active # Abort reauth config flow if active

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import logging
from homewizard_energy.models import Data, Device, State, System from homewizard_energy.models import Data, Device, State, System
@ -11,6 +12,8 @@ from homeassistant.const import Platform
DOMAIN = "homewizard" DOMAIN = "homewizard"
PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
LOGGER = logging.getLogger(__package__)
# Platform config. # Platform config.
CONF_API_ENABLED = "api_enabled" CONF_API_ENABLED = "api_enabled"
CONF_DATA = "data" CONF_DATA = "data"

View File

@ -436,7 +436,6 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Initialize sensors.""" """Initialize sensors."""
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
HomeWizardSensorEntity(coordinator, description) HomeWizardSensorEntity(coordinator, description)
for description in SENSORS for description in SENSORS

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/meteo_france", "documentation": "https://www.home-assistant.io/integrations/meteo_france",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["meteofrance_api"], "loggers": ["meteofrance_api"],
"requirements": ["meteofrance-api==1.2.0"] "requirements": ["meteofrance-api==1.3.0"]
} }

View File

@ -406,6 +406,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
values["color_temp"], values["color_temp"],
self.entity_id, self.entity_id,
) )
# Allow to switch back to color_temp
if "color" not in values:
self._attr_hs_color = None
if self.supported_features and LightEntityFeature.EFFECT: if self.supported_features and LightEntityFeature.EFFECT:
with suppress(KeyError): with suppress(KeyError):

View File

@ -59,7 +59,7 @@ class NoboGlobalSelector(SelectEntity):
nobo.API.OVERRIDE_MODE_ECO: "eco", nobo.API.OVERRIDE_MODE_ECO: "eco",
} }
_attr_options = list(_modes.values()) _attr_options = list(_modes.values())
_attr_current_option: str _attr_current_option: str | None = None
def __init__(self, hub: nobo, override_type) -> None: def __init__(self, hub: nobo, override_type) -> None:
"""Initialize the global override selector.""" """Initialize the global override selector."""
@ -117,7 +117,7 @@ class NoboProfileSelector(SelectEntity):
_attr_should_poll = False _attr_should_poll = False
_profiles: dict[int, str] = {} _profiles: dict[int, str] = {}
_attr_options: list[str] = [] _attr_options: list[str] = []
_attr_current_option: str _attr_current_option: str | None = None
def __init__(self, zone_id: str, hub: nobo) -> None: def __init__(self, zone_id: str, hub: nobo) -> None:
"""Initialize the week profile selector.""" """Initialize the week profile selector."""

View File

@ -89,7 +89,7 @@ class OurGroceriesTodoListEntity(
if item.summary: if item.summary:
api_items = self.coordinator.data[self._list_id]["list"]["items"] api_items = self.coordinator.data[self._list_id]["list"]["items"]
category = next( category = next(
api_item["categoryId"] api_item.get("categoryId")
for api_item in api_items for api_item in api_items
if api_item["id"] == item.uid if api_item["id"] == item.uid
) )

View File

@ -1,10 +1,8 @@
"""The Overkiz (by Somfy) integration.""" """The Overkiz (by Somfy) integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import cast
from aiohttp import ClientError from aiohttp import ClientError
from pyoverkiz.client import OverkizClient from pyoverkiz.client import OverkizClient
@ -16,7 +14,7 @@ from pyoverkiz.exceptions import (
NotSuchTokenException, NotSuchTokenException,
TooManyRequestsException, TooManyRequestsException,
) )
from pyoverkiz.models import Device, OverkizServer, Scenario, Setup from pyoverkiz.models import Device, OverkizServer, Scenario
from pyoverkiz.utils import generate_local_server from pyoverkiz.utils import generate_local_server
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -82,13 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
await client.login() await client.login()
setup = await client.get_setup()
setup, scenarios = await asyncio.gather( # Local API does expose scenarios, but they are not functional.
*[ # Tracked in https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode/issues/21
client.get_setup(), if api_type == APIType.CLOUD:
client.get_scenarios(), scenarios = await client.get_scenarios()
] else:
) scenarios = []
except (BadCredentialsException, NotSuchTokenException) as exception: except (BadCredentialsException, NotSuchTokenException) as exception:
raise ConfigEntryAuthFailed("Invalid authentication") from exception raise ConfigEntryAuthFailed("Invalid authentication") from exception
except TooManyRequestsException as exception: except TooManyRequestsException as exception:
@ -98,9 +97,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except MaintenanceException as exception: except MaintenanceException as exception:
raise ConfigEntryNotReady("Server is down for maintenance") from exception raise ConfigEntryNotReady("Server is down for maintenance") from exception
setup = cast(Setup, setup)
scenarios = cast(list[Scenario], scenarios)
coordinator = OverkizDataUpdateCoordinator( coordinator = OverkizDataUpdateCoordinator(
hass, hass,
LOGGER, LOGGER,

View File

@ -9,7 +9,7 @@
} }
}, },
"local_or_cloud": { "local_or_cloud": {
"description": "Choose between local or cloud API. Local API supports TaHoma Connexoon, TaHoma v2, and TaHoma Switch. Climate devices are not supported in local API.", "description": "Choose between local or cloud API. Local API supports TaHoma Connexoon, TaHoma v2, and TaHoma Switch. Climate devices and scenarios are not supported in local API.",
"data": { "data": {
"api_type": "API type" "api_type": "API type"
} }

View File

@ -33,7 +33,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
meter_verification: bool = False
meter_data: dict[str, str] = {} meter_data: dict[str, str] = {}
meter_error: dict[str, str] = {} meter_error: dict[str, str] = {}
@ -53,17 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except HttpError: except HttpError:
self.meter_error = {"phone_number": "http_error", "type": "error"} self.meter_error = {"phone_number": "http_error", "type": "error"}
self.hass.async_create_task(
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if self.meter_verification is True:
return self.async_show_progress_done(next_step_id="finish_smart_meter")
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -86,20 +78,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(f"{county}-{phone_number}") await self.async_set_unique_id(f"{county}-{phone_number}")
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
self.meter_verification = True
if self.meter_error is not None: if self.meter_error is not None:
# Clear any previous errors, since the user may have corrected them # Clear any previous errors, since the user may have corrected them
self.meter_error = {} self.meter_error = {}
self.hass.async_create_task(self._verify_meter(phone_number)) await self._verify_meter(phone_number)
self.meter_data = user_input self.meter_data = user_input
return self.async_show_progress( return await self.async_step_finish_smart_meter()
step_id="user",
progress_action="verifying_meter",
)
async def async_step_finish_smart_meter( async def async_step_finish_smart_meter(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -107,7 +94,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the finish smart meter step.""" """Handle the finish smart meter step."""
if "phone_number" in self.meter_error: if "phone_number" in self.meter_error:
if self.meter_error["type"] == "error": if self.meter_error["type"] == "error":
self.meter_verification = False
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=STEP_USER_DATA_SCHEMA, data_schema=STEP_USER_DATA_SCHEMA,

View File

@ -40,7 +40,7 @@ class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]):
hass, hass,
_LOGGER, _LOGGER,
name=f"Ping {ping.ip_address}", name=f"Ping {ping.ip_address}",
update_interval=timedelta(minutes=5), update_interval=timedelta(seconds=30),
) )
async def _async_update_data(self) -> PingResult: async def _async_update_data(self) -> PingResult:

View File

@ -160,6 +160,16 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
# Keep track of the previous action-mode # Keep track of the previous action-mode
self._previous_action_mode(self.coordinator) self._previous_action_mode(self.coordinator)
# Adam provides the hvac_action for each thermostat
if (control_state := self.device.get("control_state")) == "cooling":
return HVACAction.COOLING
if control_state == "heating":
return HVACAction.HEATING
if control_state == "preheating":
return HVACAction.PREHEATING
if control_state == "off":
return HVACAction.IDLE
heater: str = self.coordinator.data.gateway["heater_id"] heater: str = self.coordinator.data.gateway["heater_id"]
heater_data = self.coordinator.data.devices[heater] heater_data = self.coordinator.data.devices[heater]
if heater_data["binary_sensors"]["heating_state"]: if heater_data["binary_sensors"]["heating_state"]:

View File

@ -7,6 +7,6 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["crcmod", "plugwise"], "loggers": ["crcmod", "plugwise"],
"requirements": ["plugwise==0.34.3"], "requirements": ["plugwise==0.34.5"],
"zeroconf": ["_plugwise._tcp.local."] "zeroconf": ["_plugwise._tcp.local."]
} }

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.8.1"] "requirements": ["reolink-aio==0.8.2"]
} }

View File

@ -373,14 +373,14 @@ class RoonDevice(MediaPlayerEntity):
def volume_up(self) -> None: def volume_up(self) -> None:
"""Send new volume_level to device.""" """Send new volume_level to device."""
if self._volume_incremental: if self._volume_incremental:
self._server.roonapi.change_volume_raw(self.output_id, 1, "relative_step") self._server.roonapi.change_volume_raw(self.output_id, 1, "relative")
else: else:
self._server.roonapi.change_volume_percent(self.output_id, 3) self._server.roonapi.change_volume_percent(self.output_id, 3)
def volume_down(self) -> None: def volume_down(self) -> None:
"""Send new volume_level to device.""" """Send new volume_level to device."""
if self._volume_incremental: if self._volume_incremental:
self._server.roonapi.change_volume_raw(self.output_id, -1, "relative_step") self._server.roonapi.change_volume_raw(self.output_id, -1, "relative")
else: else:
self._server.roonapi.change_volume_percent(self.output_id, -3) self._server.roonapi.change_volume_percent(self.output_id, -3)

View File

@ -497,14 +497,16 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return UNIT_MAP[self._device.status.attributes[Attribute.temperature].unit] return UNIT_MAP[self._device.status.attributes[Attribute.temperature].unit]
def _determine_swing_modes(self) -> list[str]: def _determine_swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.""" """Return the list of available swing modes."""
supported_swings = None
supported_modes = self._device.status.attributes[ supported_modes = self._device.status.attributes[
Attribute.supported_fan_oscillation_modes Attribute.supported_fan_oscillation_modes
][0] ][0]
supported_swings = [ if supported_modes is not None:
FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes supported_swings = [
] FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes
]
return supported_swings return supported_swings
async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_swing_mode(self, swing_mode: str) -> None:

View File

@ -263,8 +263,8 @@ def _attach_file(hass, atch_name, content_id=""):
file_name = os.path.basename(atch_name) file_name = os.path.basename(atch_name)
url = "https://www.home-assistant.io/docs/configuration/basic/" url = "https://www.home-assistant.io/docs/configuration/basic/"
raise ServiceValidationError( raise ServiceValidationError(
f"Cannot send email with attachment '{file_name} " f"Cannot send email with attachment '{file_name}' "
f"from directory '{file_path} which is not secure to load data from. " f"from directory '{file_path}' which is not secure to load data from. "
f"Only folders added to `{allow_list}` are accessible. " f"Only folders added to `{allow_list}` are accessible. "
f"See {url} for more information.", f"See {url} for more information.",
translation_domain=DOMAIN, translation_domain=DOMAIN,

View File

@ -7,7 +7,7 @@
}, },
"exceptions": { "exceptions": {
"remote_path_not_allowed": { "remote_path_not_allowed": {
"message": "Cannot send email with attachment '{file_name} form directory '{file_path} which is not secure to load data from. Only folders added to `{allow_list}` are accessible. See {url} for more information." "message": "Cannot send email with attachment \"{file_name}\" from directory \"{file_path}\" which is not secure to load data from. Only folders added to `{allow_list}` are accessible. See {url} for more information."
} }
} }
} }

View File

@ -362,6 +362,8 @@ class SQLSensor(ManualTriggerSensorEntity):
self._query, self._query,
redact_credentials(str(err)), redact_credentials(str(err)),
) )
sess.rollback()
sess.close()
return return
for res in result.mappings(): for res in result.mappings():

View File

@ -41,7 +41,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"], "loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.21.0", "unifi-discovery==1.1.7"], "requirements": ["pyunifiprotect==4.22.0", "unifi-discovery==1.1.7"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/weatherkit", "documentation": "https://www.home-assistant.io/integrations/weatherkit",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["apple_weatherkit==1.1.1"] "requirements": ["apple_weatherkit==1.1.2"]
} }

View File

@ -209,21 +209,26 @@ class IsWorkdaySensor(BinarySensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get date and look whether it is a holiday.""" """Get date and look whether it is a holiday."""
self._attr_is_on = self.date_is_workday(dt_util.now())
async def check_date(self, check_date: date) -> ServiceResponse:
"""Service to check if date is workday or not."""
return {"workday": self.date_is_workday(check_date)}
def date_is_workday(self, check_date: date) -> bool:
"""Check if date is workday."""
# Default is no workday # Default is no workday
self._attr_is_on = False is_workday = False
# Get ISO day of the week (1 = Monday, 7 = Sunday) # Get ISO day of the week (1 = Monday, 7 = Sunday)
adjusted_date = dt_util.now() + timedelta(days=self._days_offset) adjusted_date = check_date + timedelta(days=self._days_offset)
day = adjusted_date.isoweekday() - 1 day = adjusted_date.isoweekday() - 1
day_of_week = ALLOWED_DAYS[day] day_of_week = ALLOWED_DAYS[day]
if self.is_include(day_of_week, adjusted_date): if self.is_include(day_of_week, adjusted_date):
self._attr_is_on = True is_workday = True
if self.is_exclude(day_of_week, adjusted_date): if self.is_exclude(day_of_week, adjusted_date):
self._attr_is_on = False is_workday = False
async def check_date(self, check_date: date) -> ServiceResponse: return is_workday
"""Check if date is workday or not."""
holiday_date = check_date in self._obj_holidays
return {"workday": not holiday_date}

View File

@ -6,6 +6,6 @@
"dependencies": ["assist_pipeline"], "dependencies": ["assist_pipeline"],
"documentation": "https://www.home-assistant.io/integrations/wyoming", "documentation": "https://www.home-assistant.io/integrations/wyoming",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["wyoming==1.3.0"], "requirements": ["wyoming==1.4.0"],
"zeroconf": ["_wyoming._tcp.local."] "zeroconf": ["_wyoming._tcp.local."]
} }

View File

@ -9,6 +9,7 @@ import wave
from wyoming.asr import Transcribe, Transcript from wyoming.asr import Transcribe, Transcript
from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop
from wyoming.client import AsyncTcpClient from wyoming.client import AsyncTcpClient
from wyoming.error import Error
from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import RunSatellite from wyoming.satellite import RunSatellite
from wyoming.tts import Synthesize, SynthesizeVoice from wyoming.tts import Synthesize, SynthesizeVoice
@ -227,6 +228,7 @@ class WyomingSatellite:
end_stage=end_stage, end_stage=end_stage,
tts_audio_output="wav", tts_audio_output="wav",
pipeline_id=pipeline_id, pipeline_id=pipeline_id,
device_id=self.device.device_id,
) )
) )
@ -321,6 +323,16 @@ class WyomingSatellite:
if event.data and (tts_output := event.data["tts_output"]): if event.data and (tts_output := event.data["tts_output"]):
media_id = tts_output["media_id"] media_id = tts_output["media_id"]
self.hass.add_job(self._stream_tts(media_id)) self.hass.add_job(self._stream_tts(media_id))
elif event.type == assist_pipeline.PipelineEventType.ERROR:
# Pipeline error
if event.data:
self.hass.add_job(
self._client.write_event(
Error(
text=event.data["message"], code=event.data["code"]
).event()
)
)
async def _connect(self) -> None: async def _connect(self) -> None:
"""Connect to satellite over TCP.""" """Connect to satellite over TCP."""

View File

@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"], "dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink", "documentation": "https://www.home-assistant.io/integrations/yolink",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"requirements": ["yolink-api==0.3.1"] "requirements": ["yolink-api==0.3.4"]
} }

View File

@ -253,7 +253,7 @@ class MatchRule:
else: else:
matches.append(model in self.models) matches.append(model in self.models)
if self.quirk_ids and quirk_id: if self.quirk_ids:
if callable(self.quirk_ids): if callable(self.quirk_ids):
matches.append(self.quirk_ids(quirk_id)) matches.append(self.quirk_ids(quirk_id))
else: else:

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 = 12 MINOR_VERSION: Final = 12
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

@ -2125,6 +2125,10 @@ def to_json(
option = ( option = (
ORJSON_PASSTHROUGH_OPTIONS ORJSON_PASSTHROUGH_OPTIONS
# OPT_NON_STR_KEYS is added as a workaround to
# ensure subclasses of str are allowed as dict keys
# See: https://github.com/ijl/orjson/issues/445
| orjson.OPT_NON_STR_KEYS
| (orjson.OPT_INDENT_2 if pretty_print else 0) | (orjson.OPT_INDENT_2 if pretty_print else 0)
| (orjson.OPT_SORT_KEYS if sort_keys else 0) | (orjson.OPT_SORT_KEYS if sort_keys else 0)
) )

View File

@ -26,7 +26,7 @@ ha-ffmpeg==3.1.0
hass-nabucasa==0.74.0 hass-nabucasa==0.74.0
hassil==1.5.1 hassil==1.5.1
home-assistant-bluetooth==1.10.4 home-assistant-bluetooth==1.10.4
home-assistant-frontend==20231206.0 home-assistant-frontend==20231208.2
home-assistant-intents==2023.12.05 home-assistant-intents==2023.12.05
httpx==0.25.0 httpx==0.25.0
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -33,9 +33,17 @@ class SerializationError(HomeAssistantError):
"""Error serializing the data to JSON.""" """Error serializing the data to JSON."""
json_loads: Callable[[bytes | bytearray | memoryview | str], JsonValueType] def json_loads(__obj: bytes | bytearray | memoryview | str) -> JsonValueType:
json_loads = orjson.loads """Parse JSON data.
"""Parse JSON data."""
This adds a workaround for orjson not handling subclasses of str,
https://github.com/ijl/orjson/issues/445.
"""
if type(__obj) in (bytes, bytearray, memoryview, str):
return orjson.loads(__obj) # type:ignore[no-any-return]
if isinstance(__obj, str):
return orjson.loads(str(__obj)) # type:ignore[no-any-return]
return orjson.loads(__obj) # type:ignore[no-any-return]
def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayType: def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayType:

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.12.0" version = "2023.12.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"

View File

@ -437,7 +437,7 @@ anthemav==1.4.1
apcaccess==0.0.13 apcaccess==0.0.13
# homeassistant.components.weatherkit # homeassistant.components.weatherkit
apple_weatherkit==1.1.1 apple_weatherkit==1.1.2
# homeassistant.components.apprise # homeassistant.components.apprise
apprise==1.6.0 apprise==1.6.0
@ -1014,7 +1014,7 @@ hole==0.8.0
holidays==0.36 holidays==0.36
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20231206.0 home-assistant-frontend==20231208.2
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.12.05 home-assistant-intents==2023.12.05
@ -1234,7 +1234,7 @@ messagebird==1.2.0
meteoalertapi==0.3.0 meteoalertapi==0.3.0
# homeassistant.components.meteo_france # homeassistant.components.meteo_france
meteofrance-api==1.2.0 meteofrance-api==1.3.0
# homeassistant.components.mfi # homeassistant.components.mfi
mficlient==0.3.0 mficlient==0.3.0
@ -1476,7 +1476,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14 plexwebsocket==0.0.14
# homeassistant.components.plugwise # homeassistant.components.plugwise
plugwise==0.34.3 plugwise==0.34.5
# homeassistant.components.plum_lightpad # homeassistant.components.plum_lightpad
plumlightpad==0.0.11 plumlightpad==0.0.11
@ -2245,7 +2245,7 @@ pytrydan==0.4.0
pyudev==0.23.2 pyudev==0.23.2
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==4.21.0 pyunifiprotect==4.22.0
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -2338,7 +2338,7 @@ renault-api==0.2.0
renson-endura-delta==1.6.0 renson-endura-delta==1.6.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.1 reolink-aio==0.8.2
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2750,7 +2750,7 @@ wled==0.17.0
wolf-smartset==0.1.11 wolf-smartset==0.1.11
# homeassistant.components.wyoming # homeassistant.components.wyoming
wyoming==1.3.0 wyoming==1.4.0
# homeassistant.components.xbox # homeassistant.components.xbox
xbox-webapi==2.0.11 xbox-webapi==2.0.11
@ -2792,7 +2792,7 @@ yeelight==0.7.14
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.1 yolink-api==0.3.4
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1

View File

@ -401,7 +401,7 @@ anthemav==1.4.1
apcaccess==0.0.13 apcaccess==0.0.13
# homeassistant.components.weatherkit # homeassistant.components.weatherkit
apple_weatherkit==1.1.1 apple_weatherkit==1.1.2
# homeassistant.components.apprise # homeassistant.components.apprise
apprise==1.6.0 apprise==1.6.0
@ -801,7 +801,7 @@ hole==0.8.0
holidays==0.36 holidays==0.36
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20231206.0 home-assistant-frontend==20231208.2
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.12.05 home-assistant-intents==2023.12.05
@ -958,7 +958,7 @@ medcom-ble==0.1.1
melnor-bluetooth==0.0.25 melnor-bluetooth==0.0.25
# homeassistant.components.meteo_france # homeassistant.components.meteo_france
meteofrance-api==1.2.0 meteofrance-api==1.3.0
# homeassistant.components.mfi # homeassistant.components.mfi
mficlient==0.3.0 mficlient==0.3.0
@ -1134,7 +1134,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14 plexwebsocket==0.0.14
# homeassistant.components.plugwise # homeassistant.components.plugwise
plugwise==0.34.3 plugwise==0.34.5
# homeassistant.components.plum_lightpad # homeassistant.components.plum_lightpad
plumlightpad==0.0.11 plumlightpad==0.0.11
@ -1681,7 +1681,7 @@ pytrydan==0.4.0
pyudev==0.23.2 pyudev==0.23.2
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==4.21.0 pyunifiprotect==4.22.0
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1750,7 +1750,7 @@ renault-api==0.2.0
renson-endura-delta==1.6.0 renson-endura-delta==1.6.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.1 reolink-aio==0.8.2
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
@ -2054,7 +2054,7 @@ wled==0.17.0
wolf-smartset==0.1.11 wolf-smartset==0.1.11
# homeassistant.components.wyoming # homeassistant.components.wyoming
wyoming==1.3.0 wyoming==1.4.0
# homeassistant.components.xbox # homeassistant.components.xbox
xbox-webapi==2.0.11 xbox-webapi==2.0.11
@ -2090,7 +2090,7 @@ yalexs==1.10.0
yeelight==0.7.14 yeelight==0.7.14
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.1 yolink-api==0.3.4
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1

View File

@ -1,4 +1,5 @@
"""Test Voice Assistant init.""" """Test Voice Assistant init."""
import asyncio
from dataclasses import asdict from dataclasses import asdict
import itertools as it import itertools as it
from pathlib import Path from pathlib import Path
@ -569,6 +570,69 @@ async def test_pipeline_saved_audio_write_error(
) )
async def test_pipeline_saved_audio_empty_queue(
hass: HomeAssistant,
mock_stt_provider: MockSttProvider,
mock_wake_word_provider_entity: MockWakeWordEntity,
init_supporting_components,
snapshot: SnapshotAssertion,
) -> None:
"""Test that saved audio thread closes WAV file even if there's an empty queue."""
with tempfile.TemporaryDirectory() as temp_dir_str:
# Enable audio recording to temporary directory
temp_dir = Path(temp_dir_str)
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}},
)
def event_callback(event: assist_pipeline.PipelineEvent):
if event.type == "run-end":
# Verify WAV file exists, but contains no data
pipeline_dirs = list(temp_dir.iterdir())
run_dirs = list(pipeline_dirs[0].iterdir())
wav_path = next(run_dirs[0].iterdir())
with wave.open(str(wav_path), "rb") as wav_file:
assert wav_file.getnframes() == 0
async def audio_data():
# Force timeout in _pipeline_debug_recording_thread_proc
await asyncio.sleep(1)
yield b"not used"
# Wrap original function to time out immediately
_pipeline_debug_recording_thread_proc = (
assist_pipeline.pipeline._pipeline_debug_recording_thread_proc
)
def proc_wrapper(run_recording_dir, queue):
_pipeline_debug_recording_thread_proc(
run_recording_dir, queue, message_timeout=0
)
with patch(
"homeassistant.components.assist_pipeline.pipeline._pipeline_debug_recording_thread_proc",
proc_wrapper,
):
await assist_pipeline.async_pipeline_from_audio_stream(
hass,
context=Context(),
event_callback=event_callback,
stt_metadata=stt.SpeechMetadata(
language="",
format=stt.AudioFormats.WAV,
codec=stt.AudioCodecs.PCM,
bit_rate=stt.AudioBitRates.BITRATE_16,
sample_rate=stt.AudioSampleRates.SAMPLERATE_16000,
channel=stt.AudioChannels.CHANNEL_MONO,
),
stt_stream=audio_data(),
start_stage=assist_pipeline.PipelineStage.WAKE_WORD,
end_stage=assist_pipeline.PipelineStage.STT,
)
async def test_wake_word_detection_aborted( async def test_wake_word_detection_aborted(
hass: HomeAssistant, hass: HomeAssistant,
mock_stt_provider: MockSttProvider, mock_stt_provider: MockSttProvider,

View File

@ -14,9 +14,9 @@ from .common import ASUSWRT_BASE, MOCK_MACS, ROUTER_MAC_ADDR, new_device
ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtHttp" ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtHttp"
ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy" ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy"
MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_BYTES_TOTAL = 60000000000, 50000000000
MOCK_BYTES_TOTAL_HTTP = dict(enumerate(MOCK_BYTES_TOTAL)) MOCK_BYTES_TOTAL_HTTP = dict(enumerate(MOCK_BYTES_TOTAL))
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000
MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES)) MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES))
MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3} MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3}
MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values()) MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values())

View File

@ -307,8 +307,8 @@ async def test_device_area_context(
turn_on_calls = async_mock_service(hass, "light", "turn_on") turn_on_calls = async_mock_service(hass, "light", "turn_on")
turn_off_calls = async_mock_service(hass, "light", "turn_off") turn_off_calls = async_mock_service(hass, "light", "turn_off")
area_kitchen = area_registry.async_get_or_create("kitchen") area_kitchen = area_registry.async_get_or_create("Kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom") area_bedroom = area_registry.async_get_or_create("Bedroom")
# Create 2 lights in each area # Create 2 lights in each area
area_lights = defaultdict(list) area_lights = defaultdict(list)
@ -323,7 +323,7 @@ async def test_device_area_context(
"off", "off",
attributes={ATTR_FRIENDLY_NAME: f"{area.name} light {i}"}, attributes={ATTR_FRIENDLY_NAME: f"{area.name} light {i}"},
) )
area_lights[area.name].append(light_entity) area_lights[area.id].append(light_entity)
# Create voice satellites in each area # Create voice satellites in each area
entry = MockConfigEntry() entry = MockConfigEntry()
@ -354,6 +354,8 @@ async def test_device_area_context(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
# Verify only kitchen lights were targeted # Verify only kitchen lights were targeted
assert {s.entity_id for s in result.response.matched_states} == { assert {s.entity_id for s in result.response.matched_states} == {
@ -375,6 +377,8 @@ async def test_device_area_context(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
# Verify only bedroom lights were targeted # Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == { assert {s.entity_id for s in result.response.matched_states} == {
@ -396,6 +400,8 @@ async def test_device_area_context(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
# Verify only bedroom lights were targeted # Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == { assert {s.entity_id for s in result.response.matched_states} == {

View File

@ -186,7 +186,6 @@ async def test_no_lights_or_groups(
"state": STATE_ON, "state": STATE_ON,
"attributes": { "attributes": {
ATTR_EFFECT_LIST: [ ATTR_EFFECT_LIST: [
EFFECT_COLORLOOP,
"carnival", "carnival",
"collide", "collide",
"fading", "fading",

View File

@ -67,7 +67,7 @@ LAST_READING = Reading(
"energyOut": 55048723044000.0, "energyOut": 55048723044000.0,
"energyOut1": 0.0, "energyOut1": 0.0,
"energyOut2": 0.0, "energyOut2": 0.0,
"power": 531750.0, "power": 0.0,
"power1": 142680.0, "power1": 142680.0,
"power2": 138010.0, "power2": 138010.0,
"power3": 251060.0, "power3": 251060.0,

View File

@ -61,7 +61,7 @@
'energyOut': 55048723044000.0, 'energyOut': 55048723044000.0,
'energyOut1': 0.0, 'energyOut1': 0.0,
'energyOut2': 0.0, 'energyOut2': 0.0,
'power': 531750.0, 'power': 0.0,
'power1': 142680.0, 'power1': 142680.0,
'power2': 138010.0, 'power2': 138010.0,
'power3': 251060.0, 'power3': 251060.0,

View File

@ -132,7 +132,7 @@
'entity_id': 'sensor.electricity_teststrasse_1_total_power', 'entity_id': 'sensor.electricity_teststrasse_1_total_power',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '531.75', 'state': '0.0',
}) })
# --- # ---
# name: test_sensor[gas last transmitted] # name: test_sensor[gas last transmitted]

View File

@ -877,6 +877,114 @@ async def test_cost_sensor_handle_price_units(
assert state.state == "20.0" assert state.state == "20.0"
async def test_cost_sensor_handle_late_price_sensor(
setup_integration,
hass: HomeAssistant,
hass_storage: dict[str, Any],
) -> None:
"""Test energy cost where the price sensor is not immediately available."""
energy_attributes = {
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
}
price_attributes = {
ATTR_UNIT_OF_MEASUREMENT: f"EUR/{UnitOfEnergy.KILO_WATT_HOUR}",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
}
energy_data = data.EnergyManager.default_preferences()
energy_data["energy_sources"].append(
{
"type": "grid",
"flow_from": [
{
"stat_energy_from": "sensor.energy_consumption",
"stat_cost": None,
"entity_energy_price": "sensor.energy_price",
"number_energy_price": None,
}
],
"flow_to": [],
"cost_adjustment_day": 0,
}
)
hass_storage[data.STORAGE_KEY] = {
"version": 1,
"data": energy_data,
}
# Initial state: 10kWh, price sensor not yet available
hass.states.async_set("sensor.energy_price", "unknown", price_attributes)
hass.states.async_set(
"sensor.energy_consumption",
10,
energy_attributes,
)
await setup_integration(hass)
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "0.0"
# Energy use bumped by 10 kWh, price sensor still not yet available
hass.states.async_set(
"sensor.energy_consumption",
20,
energy_attributes,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "0.0"
# Energy use bumped by 10 kWh, price sensor now available
hass.states.async_set("sensor.energy_price", "1", price_attributes)
hass.states.async_set(
"sensor.energy_consumption",
30,
energy_attributes,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "20.0"
# Energy use bumped by 10 kWh, price sensor available
hass.states.async_set(
"sensor.energy_consumption",
40,
energy_attributes,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "30.0"
# Energy use bumped by 10 kWh, price sensor no longer available
hass.states.async_set("sensor.energy_price", "unknown", price_attributes)
hass.states.async_set(
"sensor.energy_consumption",
50,
energy_attributes,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "30.0"
# Energy use bumped by 10 kWh, price sensor again available
hass.states.async_set("sensor.energy_price", "2", price_attributes)
hass.states.async_set(
"sensor.energy_consumption",
60,
energy_attributes,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_consumption_cost")
assert state.state == "70.0"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"unit", "unit",
(UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), (UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),

View File

@ -7,7 +7,9 @@ import pytest
from homeassistant.components.homewizard.const import DOMAIN from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -118,3 +120,104 @@ async def test_load_handles_homewizardenergy_exception(
ConfigEntryState.SETUP_RETRY, ConfigEntryState.SETUP_RETRY,
ConfigEntryState.SETUP_ERROR, ConfigEntryState.SETUP_ERROR,
) )
@pytest.mark.parametrize(
("device_fixture", "old_unique_id", "new_unique_id"),
[
(
"HWE-SKT",
"aabbccddeeff_total_power_import_t1_kwh",
"aabbccddeeff_total_power_import_kwh",
),
(
"HWE-SKT",
"aabbccddeeff_total_power_export_t1_kwh",
"aabbccddeeff_total_power_export_kwh",
),
],
)
@pytest.mark.usefixtures("mock_homewizardenergy")
async def test_sensor_migration(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
old_unique_id: str,
new_unique_id: str,
) -> None:
"""Test total power T1 sensors are migrated."""
mock_config_entry.add_to_hass(hass)
entity: er.RegistryEntry = entity_registry.async_get_or_create(
domain=Platform.SENSOR,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=mock_config_entry,
)
assert entity.unique_id == old_unique_id
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id)
assert entity_migrated
assert entity_migrated.unique_id == new_unique_id
assert entity_migrated.previous_unique_id == old_unique_id
@pytest.mark.parametrize(
("device_fixture", "old_unique_id", "new_unique_id"),
[
(
"HWE-SKT",
"aabbccddeeff_total_power_import_t1_kwh",
"aabbccddeeff_total_power_import_kwh",
),
(
"HWE-SKT",
"aabbccddeeff_total_power_export_t1_kwh",
"aabbccddeeff_total_power_export_kwh",
),
],
)
@pytest.mark.usefixtures("mock_homewizardenergy")
async def test_sensor_migration_does_not_trigger(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
old_unique_id: str,
new_unique_id: str,
) -> None:
"""Test total power T1 sensors are not migrated when not possible."""
mock_config_entry.add_to_hass(hass)
old_entity: er.RegistryEntry = entity_registry.async_get_or_create(
domain=Platform.SENSOR,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=mock_config_entry,
)
new_entity: er.RegistryEntry = entity_registry.async_get_or_create(
domain=Platform.SENSOR,
platform=DOMAIN,
unique_id=new_unique_id,
config_entry=mock_config_entry,
)
assert old_entity.unique_id == old_unique_id
assert new_entity.unique_id == new_unique_id
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity = entity_registry.async_get(old_entity.entity_id)
assert entity
assert entity.unique_id == old_unique_id
assert entity.previous_unique_id is None
entity = entity_registry.async_get(new_entity.entity_id)
assert entity
assert entity.unique_id == new_unique_id
assert entity.previous_unique_id is None

View File

@ -725,6 +725,93 @@ async def test_controlling_state_via_topic2(
) )
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
light.DOMAIN: {
"schema": "json",
"name": "test",
"command_topic": "test_light_rgb/set",
"state_topic": "test_light_rgb/set",
"rgb": True,
"color_temp": True,
"brightness": True,
}
}
}
],
)
async def test_controlling_the_state_with_legacy_color_handling(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test state updates for lights with a legacy color handling."""
supported_color_modes = ["color_temp", "hs"]
await mqtt_mock_entry()
state = hass.states.get("light.test")
assert state.state == STATE_UNKNOWN
expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
assert state.attributes.get("brightness") is None
assert state.attributes.get("color_mode") is None
assert state.attributes.get("color_temp") is None
assert state.attributes.get("effect") is None
assert state.attributes.get("hs_color") is None
assert state.attributes.get("rgb_color") is None
assert state.attributes.get("rgbw_color") is None
assert state.attributes.get("rgbww_color") is None
assert state.attributes.get("supported_color_modes") == supported_color_modes
assert state.attributes.get("xy_color") is None
assert not state.attributes.get(ATTR_ASSUMED_STATE)
for _ in range(0, 2):
# Returned state after the light was turned on
# Receiving legacy color mode: rgb.
async_fire_mqtt_message(
hass,
"test_light_rgb/set",
'{ "state": "ON", "brightness": 255, "level": 100, "hue": 16,'
'"saturation": 100, "color": { "r": 255, "g": 67, "b": 0 }, '
'"bulb_mode": "color", "color_mode": "rgb" }',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 255
assert state.attributes.get("color_mode") == "hs"
assert state.attributes.get("color_temp") is None
assert state.attributes.get("effect") is None
assert state.attributes.get("hs_color") == (15.765, 100.0)
assert state.attributes.get("rgb_color") == (255, 67, 0)
assert state.attributes.get("rgbw_color") is None
assert state.attributes.get("rgbww_color") is None
assert state.attributes.get("xy_color") == (0.674, 0.322)
# Returned state after the lights color mode was changed
# Receiving legacy color mode: color_temp
async_fire_mqtt_message(
hass,
"test_light_rgb/set",
'{ "state": "ON", "brightness": 255, "level": 100, '
'"kelvin": 92, "color_temp": 353, "bulb_mode": "white", '
'"color_mode": "color_temp" }',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 255
assert state.attributes.get("color_mode") == "color_temp"
assert state.attributes.get("color_temp") == 353
assert state.attributes.get("effect") is None
assert state.attributes.get("hs_color") == (28.125, 61.661)
assert state.attributes.get("rgb_color") == (255, 171, 97)
assert state.attributes.get("rgbw_color") is None
assert state.attributes.get("rgbww_color") is None
assert state.attributes.get("xy_color") == (0.513, 0.386)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"hass_config", "hass_config",
[ [

View File

@ -142,12 +142,20 @@ async def test_update_todo_item_status(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("items"), [[{"id": "12345", "name": "Soda", "categoryId": "test_category"}]] ("items", "category"),
[
(
[{"id": "12345", "name": "Soda", "categoryId": "test_category"}],
"test_category",
),
([{"id": "12345", "name": "Uncategorized"}], None),
],
) )
async def test_update_todo_item_summary( async def test_update_todo_item_summary(
hass: HomeAssistant, hass: HomeAssistant,
setup_integration: None, setup_integration: None,
ourgroceries: AsyncMock, ourgroceries: AsyncMock,
category: str | None,
) -> None: ) -> None:
"""Test for updating an item summary.""" """Test for updating an item summary."""
@ -171,7 +179,7 @@ async def test_update_todo_item_summary(
) )
assert ourgroceries.change_item_on_list assert ourgroceries.change_item_on_list
args = ourgroceries.change_item_on_list.call_args args = ourgroceries.change_item_on_list.call_args
assert args.args == ("test_list", "12345", "test_category", "Milk") assert args.args == ("test_list", "12345", category, "Milk")
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -78,12 +78,6 @@ async def test_meter_value_error(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "user"
assert result["progress_action"] == "verifying_meter"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {"phone_number": "invalid_phone_number"} assert result["errors"] == {"phone_number": "invalid_phone_number"}
@ -107,12 +101,6 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "user"
assert result["progress_action"] == "verifying_meter"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.ABORT assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "incompatible_meter" assert result["reason"] == "incompatible_meter"
@ -135,12 +123,6 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "user"
assert result["progress_action"] == "verifying_meter"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {"phone_number": "unresponsive_meter"} assert result["errors"] == {"phone_number": "unresponsive_meter"}
@ -164,12 +146,6 @@ async def test_meter_http_error(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "user"
assert result["progress_action"] == "verifying_meter"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {"phone_number": "http_error"} assert result["errors"] == {"phone_number": "http_error"}
@ -193,12 +169,6 @@ async def test_smart_meter(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "user"
assert result["progress_action"] == "verifying_meter"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.CREATE_ENTRY assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Philadelphia - 1234567890" assert result["title"] == "Philadelphia - 1234567890"
assert result["data"]["phone_number"] == "1234567890" assert result["data"]["phone_number"] == "1234567890"

View File

@ -4,6 +4,7 @@
"active_preset": "no_frost", "active_preset": "no_frost",
"available": true, "available": true,
"available_schedules": ["None"], "available_schedules": ["None"],
"control_state": "off",
"dev_class": "zone_thermostat", "dev_class": "zone_thermostat",
"firmware": "2016-10-27T02:00:00+02:00", "firmware": "2016-10-27T02:00:00+02:00",
"hardware": "255", "hardware": "255",
@ -99,6 +100,7 @@
"active_preset": "home", "active_preset": "home",
"available": true, "available": true,
"available_schedules": ["None"], "available_schedules": ["None"],
"control_state": "off",
"dev_class": "zone_thermostat", "dev_class": "zone_thermostat",
"firmware": "2016-10-27T02:00:00+02:00", "firmware": "2016-10-27T02:00:00+02:00",
"hardware": "255", "hardware": "255",
@ -155,6 +157,7 @@
"active_preset": "home", "active_preset": "home",
"available": true, "available": true,
"available_schedules": ["None"], "available_schedules": ["None"],
"control_state": "off",
"dev_class": "zone_thermostat", "dev_class": "zone_thermostat",
"firmware": "2016-10-27T02:00:00+02:00", "firmware": "2016-10-27T02:00:00+02:00",
"hardware": "255", "hardware": "255",
@ -265,6 +268,7 @@
"active_preset": "home", "active_preset": "home",
"available": true, "available": true,
"available_schedules": ["None"], "available_schedules": ["None"],
"control_state": "off",
"dev_class": "zone_thermometer", "dev_class": "zone_thermometer",
"firmware": "2020-09-01T02:00:00+02:00", "firmware": "2020-09-01T02:00:00+02:00",
"hardware": "1", "hardware": "1",
@ -300,6 +304,7 @@
"cooling_present": false, "cooling_present": false,
"gateway_id": "b5c2386c6f6342669e50fe49dd05b188", "gateway_id": "b5c2386c6f6342669e50fe49dd05b188",
"heater_id": "e4684553153b44afbef2200885f379dc", "heater_id": "e4684553153b44afbef2200885f379dc",
"item_count": 219,
"notifications": {}, "notifications": {},
"smile_name": "Adam" "smile_name": "Adam"
} }

View File

@ -0,0 +1,13 @@
[
"b5c2386c6f6342669e50fe49dd05b188",
"e4684553153b44afbef2200885f379dc",
"a6abc6a129ee499c88a4d420cc413b47",
"1346fbd8498d4dbcab7e18d51b771f3d",
"833de10f269c4deab58fb9df69901b4e",
"6f3e9d7084214c21b9dfa46f6eeb8700",
"f61f1a2535f54f52ad006a3d18e459ca",
"d4496250d0e942cfa7aea3476e9070d5",
"356b65335e274d769c338223e7af9c33",
"1da4d325838e4ad8aac12177214505c9",
"457ce8414de24596a2d5e7dbc9c7682f"
]

View File

@ -468,6 +468,7 @@
"cooling_present": false, "cooling_present": false,
"gateway_id": "fe799307f1624099878210aa0b9f1475", "gateway_id": "fe799307f1624099878210aa0b9f1475",
"heater_id": "90986d591dcd426cae3ec3e8111ff730", "heater_id": "90986d591dcd426cae3ec3e8111ff730",
"item_count": 315,
"notifications": { "notifications": {
"af82e4ccf9c548528166d38e560662a4": { "af82e4ccf9c548528166d38e560662a4": {
"warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device."

View File

@ -0,0 +1,20 @@
[
"fe799307f1624099878210aa0b9f1475",
"90986d591dcd426cae3ec3e8111ff730",
"df4a4a8169904cdb9c03d61a21f42140",
"b310b72a0e354bfab43089919b9a88bf",
"a2c3583e0a6349358998b760cea82d2a",
"b59bcebaf94b499ea7d46e4a66fb62d8",
"d3da73bde12a47d5a6b8f9dad971f2ec",
"21f2b542c49845e6bb416884c55778d6",
"78d1126fc4c743db81b61c20e88342a7",
"cd0ddb54ef694e11ac18ed1cbce5dbbd",
"4a810418d5394b3f82727340b91ba740",
"02cf28bfec924855854c544690a609ef",
"a28f588dc4a049a483fd03a30361ad3a",
"6a3bf693d05e48e0b460c815a4fdd09d",
"680423ff840043738f42cc7f1ff97a36",
"f1fee6043d3642a9b0a65297455f008e",
"675416a629f343c495449970e2ca37b5",
"e7693eb9582644e5b865dba8d4447cf1"
]

View File

@ -0,0 +1,5 @@
[
"015ae9ea3f964e668e490fa39da3870b",
"1cbf783bb11e4a7c8a6843dee3a86927",
"3cb70739631c4d17a86b8b12e8a5161b"
]

View File

@ -53,6 +53,7 @@
"active_preset": "asleep", "active_preset": "asleep",
"available": true, "available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"], "available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "cooling",
"dev_class": "thermostat", "dev_class": "thermostat",
"location": "f2bf9048bef64cc5b6d5110154e33c81", "location": "f2bf9048bef64cc5b6d5110154e33c81",
"mode": "cool", "mode": "cool",
@ -102,6 +103,7 @@
"active_preset": "home", "active_preset": "home",
"available": true, "available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"], "available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "off",
"dev_class": "zone_thermostat", "dev_class": "zone_thermostat",
"firmware": "2016-10-10T02:00:00+02:00", "firmware": "2016-10-10T02:00:00+02:00",
"hardware": "255", "hardware": "255",
@ -148,6 +150,7 @@
"cooling_present": true, "cooling_present": true,
"gateway_id": "da224107914542988a88561b4452b0f6", "gateway_id": "da224107914542988a88561b4452b0f6",
"heater_id": "056ee145a816487eaa69243c3280f8bf", "heater_id": "056ee145a816487eaa69243c3280f8bf",
"item_count": 145,
"notifications": {}, "notifications": {},
"smile_name": "Adam" "smile_name": "Adam"
} }

View File

@ -0,0 +1,8 @@
[
"da224107914542988a88561b4452b0f6",
"056ee145a816487eaa69243c3280f8bf",
"ad4838d7d35c4d6ea796ee12ae5aedf8",
"1772a4ea304041adb83f357b751341ff",
"e2f4322d57924fa090fbbc48b3a140dc",
"e8ef2a01ed3b4139a53bf749204fe6b4"
]

View File

@ -1,5 +1,28 @@
{ {
"devices": { "devices": {
"01234567890abcdefghijklmnopqrstu": {
"available": false,
"dev_class": "thermo_sensor",
"firmware": "2020-11-04T01:00:00+01:00",
"hardware": "1",
"location": "f871b8c4d63549319221e294e4f88074",
"model": "Tom/Floor",
"name": "Tom Badkamer",
"sensors": {
"battery": 99,
"temperature": 18.6,
"temperature_difference": 2.3,
"valve_position": 0.0
},
"temperature_offset": {
"lower_bound": -2.0,
"resolution": 0.1,
"setpoint": 0.1,
"upper_bound": 2.0
},
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A01"
},
"056ee145a816487eaa69243c3280f8bf": { "056ee145a816487eaa69243c3280f8bf": {
"available": true, "available": true,
"binary_sensors": { "binary_sensors": {
@ -58,6 +81,7 @@
"active_preset": "asleep", "active_preset": "asleep",
"available": true, "available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"], "available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "preheating",
"dev_class": "thermostat", "dev_class": "thermostat",
"location": "f2bf9048bef64cc5b6d5110154e33c81", "location": "f2bf9048bef64cc5b6d5110154e33c81",
"mode": "heat", "mode": "heat",
@ -101,6 +125,7 @@
"active_preset": "home", "active_preset": "home",
"available": true, "available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"], "available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "off",
"dev_class": "zone_thermostat", "dev_class": "zone_thermostat",
"firmware": "2016-10-10T02:00:00+02:00", "firmware": "2016-10-10T02:00:00+02:00",
"hardware": "255", "hardware": "255",
@ -147,6 +172,7 @@
"cooling_present": false, "cooling_present": false,
"gateway_id": "da224107914542988a88561b4452b0f6", "gateway_id": "da224107914542988a88561b4452b0f6",
"heater_id": "056ee145a816487eaa69243c3280f8bf", "heater_id": "056ee145a816487eaa69243c3280f8bf",
"item_count": 145,
"notifications": {}, "notifications": {},
"smile_name": "Adam" "smile_name": "Adam"
} }

View File

@ -0,0 +1,8 @@
[
"da224107914542988a88561b4452b0f6",
"056ee145a816487eaa69243c3280f8bf",
"ad4838d7d35c4d6ea796ee12ae5aedf8",
"1772a4ea304041adb83f357b751341ff",
"e2f4322d57924fa090fbbc48b3a140dc",
"e8ef2a01ed3b4139a53bf749204fe6b4"
]

View File

@ -0,0 +1,5 @@
[
"015ae9ea3f964e668e490fa39da3870b",
"1cbf783bb11e4a7c8a6843dee3a86927",
"3cb70739631c4d17a86b8b12e8a5161b"
]

View File

@ -0,0 +1,5 @@
[
"015ae9ea3f964e668e490fa39da3870b",
"1cbf783bb11e4a7c8a6843dee3a86927",
"3cb70739631c4d17a86b8b12e8a5161b"
]

View File

@ -42,6 +42,7 @@
}, },
"gateway": { "gateway": {
"gateway_id": "cd3e822288064775a7c4afcdd70bdda2", "gateway_id": "cd3e822288064775a7c4afcdd70bdda2",
"item_count": 31,
"notifications": {}, "notifications": {},
"smile_name": "Smile P1" "smile_name": "Smile P1"
} }

View File

@ -0,0 +1 @@
["cd3e822288064775a7c4afcdd70bdda2", "e950c7d5e1ee407a858e2a8b5016c8b3"]

View File

@ -51,6 +51,7 @@
}, },
"gateway": { "gateway": {
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
"item_count": 40,
"notifications": { "notifications": {
"97a04c0c263049b29350a660b4cdd01e": { "97a04c0c263049b29350a660b4cdd01e": {
"warning": "The Smile P1 is not connected to a smart meter." "warning": "The Smile P1 is not connected to a smart meter."

View File

@ -0,0 +1 @@
["03e65b16e4b247a29ae0d75a78cb492e", "b82b6b3322484f2ea4e25e0bd5f3d61f"]

View File

@ -135,6 +135,7 @@
}, },
"gateway": { "gateway": {
"gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00",
"item_count": 83,
"notifications": {}, "notifications": {},
"smile_name": "Stretch" "smile_name": "Stretch"
} }

View File

@ -0,0 +1,10 @@
[
"0000aaaa0000aaaa0000aaaa0000aa00",
"5871317346d045bc9f6b987ef25ee638",
"e1c884e7dede431dadee09506ec4f859",
"aac7b735042c4832ac9ff33aae4f453b",
"cfe95cf3de1948c0b8955125bf754614",
"059e4d03c7a34d278add5c7a4a781d19",
"d950b314e9d8499f968e6db8d82ef78c",
"d03738edfcc947f7b8f4573571d90d2d"
]

View File

@ -500,6 +500,7 @@
'cooling_present': False, 'cooling_present': False,
'gateway_id': 'fe799307f1624099878210aa0b9f1475', 'gateway_id': 'fe799307f1624099878210aa0b9f1475',
'heater_id': '90986d591dcd426cae3ec3e8111ff730', 'heater_id': '90986d591dcd426cae3ec3e8111ff730',
'item_count': 315,
'notifications': dict({ 'notifications': dict({
'af82e4ccf9c548528166d38e560662a4': dict({ 'af82e4ccf9c548528166d38e560662a4': dict({
'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.",

View File

@ -65,7 +65,7 @@ async def test_adam_2_climate_entity_attributes(
state = hass.states.get("climate.anna") state = hass.states.get("climate.anna")
assert state assert state
assert state.state == HVACMode.HEAT assert state.state == HVACMode.HEAT
assert state.attributes["hvac_action"] == "heating" assert state.attributes["hvac_action"] == "preheating"
assert state.attributes["hvac_modes"] == [ assert state.attributes["hvac_modes"] == [
HVACMode.OFF, HVACMode.OFF,
HVACMode.AUTO, HVACMode.AUTO,
@ -75,7 +75,7 @@ async def test_adam_2_climate_entity_attributes(
state = hass.states.get("climate.lisa_badkamer") state = hass.states.get("climate.lisa_badkamer")
assert state assert state
assert state.state == HVACMode.AUTO assert state.state == HVACMode.AUTO
assert state.attributes["hvac_action"] == "heating" assert state.attributes["hvac_action"] == "idle"
assert state.attributes["hvac_modes"] == [ assert state.attributes["hvac_modes"] == [
HVACMode.OFF, HVACMode.OFF,
HVACMode.AUTO, HVACMode.AUTO,
@ -101,7 +101,7 @@ async def test_adam_3_climate_entity_attributes(
data.devices["da224107914542988a88561b4452b0f6"][ data.devices["da224107914542988a88561b4452b0f6"][
"select_regulation_mode" "select_regulation_mode"
] = "heating" ] = "heating"
data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "heat" data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "heating"
data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][
"cooling_state" "cooling_state"
] = False ] = False
@ -124,7 +124,7 @@ async def test_adam_3_climate_entity_attributes(
data.devices["da224107914542988a88561b4452b0f6"][ data.devices["da224107914542988a88561b4452b0f6"][
"select_regulation_mode" "select_regulation_mode"
] = "cooling" ] = "cooling"
data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "cool" data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "cooling"
data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][
"cooling_state" "cooling_state"
] = True ] = True

View File

@ -5,6 +5,7 @@ from datetime import timedelta
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from sqlalchemy import text as sql_text from sqlalchemy import text as sql_text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@ -12,6 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError
from homeassistant.components.recorder import Recorder from homeassistant.components.recorder import Recorder
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.sql.const import CONF_QUERY, DOMAIN from homeassistant.components.sql.const import CONF_QUERY, DOMAIN
from homeassistant.components.sql.sensor import _generate_lambda_stmt
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import ( from homeassistant.const import (
CONF_ICON, CONF_ICON,
@ -21,6 +23,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -570,3 +573,48 @@ async def test_attributes_from_entry_config(
assert state.attributes["unit_of_measurement"] == "MiB" assert state.attributes["unit_of_measurement"] == "MiB"
assert "device_class" not in state.attributes assert "device_class" not in state.attributes
assert "state_class" not in state.attributes assert "state_class" not in state.attributes
async def test_query_recover_from_rollback(
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the SQL sensor."""
config = {
"db_url": "sqlite://",
"query": "SELECT 5 as value",
"column": "value",
"name": "Select value SQL query",
"unique_id": "very_unique_id",
}
await init_integration(hass, config)
platforms = async_get_platforms(hass, "sql")
sql_entity = platforms[0].entities["sensor.select_value_sql_query"]
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
assert state.attributes["value"] == 5
with patch.object(
sql_entity,
"_lambda_stmt",
_generate_lambda_stmt("Faulty syntax create operational issue"),
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert "sqlite3.OperationalError" in caplog.text
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
assert state.attributes.get("value") is None
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.select_value_sql_query")
assert state.state == "5"
assert state.attributes.get("value") == 5

View File

@ -316,6 +316,18 @@ async def test_check_date_service(
) )
assert response == {"binary_sensor.workday_sensor": {"workday": True}} assert response == {"binary_sensor.workday_sensor": {"workday": True}}
response = await hass.services.async_call(
DOMAIN,
SERVICE_CHECK_DATE,
{
"entity_id": "binary_sensor.workday_sensor",
"check_date": date(2022, 12, 17), # Saturday (no workday)
},
blocking=True,
return_response=True,
)
assert response == {"binary_sensor.workday_sensor": {"workday": False}}
async def test_language_difference_english_language( async def test_language_difference_english_language(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -6,7 +6,7 @@
'language': 'en', 'language': 'en',
}), }),
'payload': None, 'payload': None,
'type': 'transcibe', 'type': 'transcribe',
}), }),
dict({ dict({
'data': dict({ 'data': dict({

View File

@ -8,6 +8,7 @@ import wave
from wyoming.asr import Transcribe, Transcript from wyoming.asr import Transcribe, Transcript
from wyoming.audio import AudioChunk, AudioStart, AudioStop from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.error import Error
from wyoming.event import Event from wyoming.event import Event
from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import RunSatellite from wyoming.satellite import RunSatellite
@ -96,6 +97,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient):
self.tts_audio_stop_event = asyncio.Event() self.tts_audio_stop_event = asyncio.Event()
self.tts_audio_chunk: AudioChunk | None = None self.tts_audio_chunk: AudioChunk | None = None
self.error_event = asyncio.Event()
self.error: Error | None = None
self._mic_audio_chunk = AudioChunk( self._mic_audio_chunk = AudioChunk(
rate=16000, width=2, channels=1, audio=b"chunk" rate=16000, width=2, channels=1, audio=b"chunk"
).event() ).event()
@ -135,6 +139,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient):
self.tts_audio_chunk_event.set() self.tts_audio_chunk_event.set()
elif AudioStop.is_type(event.type): elif AudioStop.is_type(event.type):
self.tts_audio_stop_event.set() self.tts_audio_stop_event.set()
elif Error.is_type(event.type):
self.error = Error.from_event(event)
self.error_event.set()
async def read_event(self) -> Event | None: async def read_event(self) -> Event | None:
"""Receive.""" """Receive."""
@ -175,8 +182,9 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None:
await mock_client.connect_event.wait() await mock_client.connect_event.wait()
await mock_client.run_satellite_event.wait() await mock_client.run_satellite_event.wait()
mock_run_pipeline.assert_called() mock_run_pipeline.assert_called_once()
event_callback = mock_run_pipeline.call_args.kwargs["event_callback"] event_callback = mock_run_pipeline.call_args.kwargs["event_callback"]
assert mock_run_pipeline.call_args.kwargs.get("device_id") == device.device_id
# Start detecting wake word # Start detecting wake word
event_callback( event_callback(
@ -458,3 +466,43 @@ async def test_satellite_disconnect_during_pipeline(hass: HomeAssistant) -> None
# Sensor should have been turned off # Sensor should have been turned off
assert not device.is_active assert not device.is_active
async def test_satellite_error_during_pipeline(hass: HomeAssistant) -> None:
"""Test satellite error occurring during pipeline run."""
events = [
RunPipeline(
start_stage=PipelineStage.WAKE, end_stage=PipelineStage.TTS
).event(),
] # no audio chunks after RunPipeline
with patch(
"homeassistant.components.wyoming.data.load_wyoming_info",
return_value=SATELLITE_INFO,
), patch(
"homeassistant.components.wyoming.satellite.AsyncTcpClient",
SatelliteAsyncTcpClient(events),
) as mock_client, patch(
"homeassistant.components.wyoming.satellite.assist_pipeline.async_pipeline_from_audio_stream",
) as mock_run_pipeline:
await setup_config_entry(hass)
async with asyncio.timeout(1):
await mock_client.connect_event.wait()
await mock_client.run_satellite_event.wait()
mock_run_pipeline.assert_called_once()
event_callback = mock_run_pipeline.call_args.kwargs["event_callback"]
event_callback(
assist_pipeline.PipelineEvent(
assist_pipeline.PipelineEventType.ERROR,
{"code": "test code", "message": "test message"},
)
)
async with asyncio.timeout(1):
await mock_client.error_event.wait()
assert mock_client.error is not None
assert mock_client.error.text == "test message"
assert mock_client.error.code == "test code"

View File

@ -1233,6 +1233,22 @@ def test_to_json(hass: HomeAssistant) -> None:
with pytest.raises(TemplateError): with pytest.raises(TemplateError):
template.Template("{{ {'Foo': now()} | to_json }}", hass).async_render() template.Template("{{ {'Foo': now()} | to_json }}", hass).async_render()
# Test special case where substring class cannot be rendered
# See: https://github.com/ijl/orjson/issues/445
class MyStr(str):
pass
expected_result = '{"mykey1":11.0,"mykey2":"myvalue2","mykey3":["opt3b","opt3a"]}'
test_dict = {
MyStr("mykey2"): "myvalue2",
MyStr("mykey1"): 11.0,
MyStr("mykey3"): ["opt3b", "opt3a"],
}
actual_result = template.Template(
"{{ test_dict | to_json(sort_keys=True) }}", hass
).async_render(parse_result=False, variables={"test_dict": test_dict})
assert actual_result == expected_result
def test_to_json_ensure_ascii(hass: HomeAssistant) -> None: def test_to_json_ensure_ascii(hass: HomeAssistant) -> None:
"""Test the object to JSON string filter.""" """Test the object to JSON string filter."""

View File

@ -1,10 +1,12 @@
"""Test Home Assistant json utility functions.""" """Test Home Assistant json utility functions."""
from pathlib import Path from pathlib import Path
import orjson
import pytest import pytest
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.json import ( from homeassistant.util.json import (
json_loads,
json_loads_array, json_loads_array,
json_loads_object, json_loads_object,
load_json, load_json,
@ -153,3 +155,20 @@ async def test_deprecated_save_json(
save_json(fname, TEST_JSON_A) save_json(fname, TEST_JSON_A)
assert "uses save_json from homeassistant.util.json" in caplog.text assert "uses save_json from homeassistant.util.json" in caplog.text
assert "should be updated to use homeassistant.helpers.json module" in caplog.text assert "should be updated to use homeassistant.helpers.json module" in caplog.text
async def test_loading_derived_class():
"""Test loading data from classes derived from str."""
class MyStr(str):
pass
class MyBytes(bytes):
pass
assert json_loads('"abc"') == "abc"
assert json_loads(MyStr('"abc"')) == "abc"
assert json_loads(b'"abc"') == "abc"
with pytest.raises(orjson.JSONDecodeError):
assert json_loads(MyBytes(b'"abc"')) == "abc"