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

View File

@ -55,7 +55,9 @@ _LOGGER = logging.getLogger(__name__)
_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]]]
@ -81,7 +83,7 @@ def handle_errors_and_zip(
if isinstance(data, dict):
return dict(zip(keys, list(data.values())))
if not isinstance(data, list):
if not isinstance(data, (list, tuple)):
raise UpdateFailed("Received invalid data type")
return dict(zip(keys, data))

View File

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

View File

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

View File

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

View File

@ -317,6 +317,11 @@ class EnergyCostSensor(SensorEntity):
try:
energy_price = float(energy_price_state.state)
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
energy_price_unit: str | None = energy_price_state.attributes.get(

View File

@ -38,11 +38,9 @@ async def async_setup_entry(
FritzboxLight(
coordinator,
ain,
device.get_colors(),
device.get_color_temps(),
)
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))
@ -57,27 +55,10 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
self,
coordinator: FritzboxDataUpdateCoordinator,
ain: str,
supported_colors: dict,
supported_color_temps: list[int],
) -> None:
"""Initialize the FritzboxLight entity."""
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]] = {}
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
def is_on(self) -> bool:
@ -173,3 +154,28 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off)
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",
"integration_type": "system",
"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",
"script",
"switch",
"todo",
"vacuum",
"water_heater",
}

View File

@ -1,12 +1,61 @@
"""The Homewizard integration."""
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.helpers import entity_registry as er
from .const import DOMAIN, PLATFORMS
from .const import DOMAIN, LOGGER, PLATFORMS
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:
"""Set up Homewizard from a config entry."""
coordinator = Coordinator(hass)
@ -21,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise
await _async_migrate_entries(hass, entry)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
# Abort reauth config flow if active

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
"iot_class": "cloud_polling",
"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"],
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:
with suppress(KeyError):

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
}
},
"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": {
"api_type": "API type"
}

View File

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

View File

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

View File

@ -160,6 +160,16 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
# Keep track of the previous action-mode
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_data = self.coordinator.data.devices[heater]
if heater_data["binary_sensors"]["heating_state"]:

View File

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

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"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:
"""Send new volume_level to device."""
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:
self._server.roonapi.change_volume_percent(self.output_id, 3)
def volume_down(self) -> None:
"""Send new volume_level to device."""
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:
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 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."""
supported_swings = None
supported_modes = self._device.status.attributes[
Attribute.supported_fan_oscillation_modes
][0]
supported_swings = [
FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes
]
if supported_modes is not None:
supported_swings = [
FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes
]
return supported_swings
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)
url = "https://www.home-assistant.io/docs/configuration/basic/"
raise ServiceValidationError(
f"Cannot send email with attachment '{file_name} "
f"from directory '{file_path} which is not secure to load data from. "
f"Cannot send email with attachment '{file_name}' "
f"from directory '{file_path}' which is not secure to load data from. "
f"Only folders added to `{allow_list}` are accessible. "
f"See {url} for more information.",
translation_domain=DOMAIN,

View File

@ -7,7 +7,7 @@
},
"exceptions": {
"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,
redact_credentials(str(err)),
)
sess.rollback()
sess.close()
return
for res in result.mappings():

View File

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

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/weatherkit",
"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:
"""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
self._attr_is_on = False
is_workday = False
# 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_of_week = ALLOWED_DAYS[day]
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):
self._attr_is_on = False
is_workday = False
async def check_date(self, check_date: date) -> ServiceResponse:
"""Check if date is workday or not."""
holiday_date = check_date in self._obj_holidays
return {"workday": not holiday_date}
return is_workday

View File

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

View File

@ -9,6 +9,7 @@ import wave
from wyoming.asr import Transcribe, Transcript
from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop
from wyoming.client import AsyncTcpClient
from wyoming.error import Error
from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import RunSatellite
from wyoming.tts import Synthesize, SynthesizeVoice
@ -227,6 +228,7 @@ class WyomingSatellite:
end_stage=end_stage,
tts_audio_output="wav",
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"]):
media_id = tts_output["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:
"""Connect to satellite over TCP."""

View File

@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink",
"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:
matches.append(model in self.models)
if self.quirk_ids and quirk_id:
if self.quirk_ids:
if callable(self.quirk_ids):
matches.append(self.quirk_ids(quirk_id))
else:

View File

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

View File

@ -2125,6 +2125,10 @@ def to_json(
option = (
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_SORT_KEYS if sort_keys else 0)
)

View File

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

View File

@ -33,9 +33,17 @@ class SerializationError(HomeAssistantError):
"""Error serializing the data to JSON."""
json_loads: Callable[[bytes | bytearray | memoryview | str], JsonValueType]
json_loads = orjson.loads
"""Parse JSON data."""
def json_loads(__obj: bytes | bytearray | memoryview | str) -> JsonValueType:
"""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:

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
"""Test Voice Assistant init."""
import asyncio
from dataclasses import asdict
import itertools as it
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(
hass: HomeAssistant,
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_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_CURRENT_TRANSFER_RATES = [20000000, 10000000]
MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000
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 = 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_off_calls = async_mock_service(hass, "light", "turn_off")
area_kitchen = area_registry.async_get_or_create("kitchen")
area_bedroom = area_registry.async_get_or_create("bedroom")
area_kitchen = area_registry.async_get_or_create("Kitchen")
area_bedroom = area_registry.async_get_or_create("Bedroom")
# Create 2 lights in each area
area_lights = defaultdict(list)
@ -323,7 +323,7 @@ async def test_device_area_context(
"off",
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
entry = MockConfigEntry()
@ -354,6 +354,8 @@ async def test_device_area_context(
)
await hass.async_block_till_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
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()
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
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()
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
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,
"attributes": {
ATTR_EFFECT_LIST: [
EFFECT_COLORLOOP,
"carnival",
"collide",
"fading",

View File

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

View File

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

View File

@ -132,7 +132,7 @@
'entity_id': 'sensor.electricity_teststrasse_1_total_power',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '531.75',
'state': '0.0',
})
# ---
# 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"
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(
"unit",
(UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),

View File

@ -7,7 +7,9 @@ import pytest
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
@ -118,3 +120,104 @@ async def test_load_handles_homewizardenergy_exception(
ConfigEntryState.SETUP_RETRY,
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(
"hass_config",
[

View File

@ -142,12 +142,20 @@ async def test_update_todo_item_status(
@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(
hass: HomeAssistant,
setup_integration: None,
ourgroceries: AsyncMock,
category: str | None,
) -> None:
"""Test for updating an item summary."""
@ -171,7 +179,7 @@ async def test_update_todo_item_summary(
)
assert ourgroceries.change_item_on_list
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(

View File

@ -78,12 +78,6 @@ async def test_meter_value_error(hass: HomeAssistant) -> None:
)
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["step_id"] == "user"
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()
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["reason"] == "incompatible_meter"
@ -135,12 +123,6 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None:
)
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["step_id"] == "user"
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()
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["step_id"] == "user"
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()
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["title"] == "Philadelphia - 1234567890"
assert result["data"]["phone_number"] == "1234567890"

View File

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

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

View File

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

View File

@ -1,5 +1,28 @@
{
"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": {
"available": true,
"binary_sensors": {
@ -58,6 +81,7 @@
"active_preset": "asleep",
"available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "preheating",
"dev_class": "thermostat",
"location": "f2bf9048bef64cc5b6d5110154e33c81",
"mode": "heat",
@ -101,6 +125,7 @@
"active_preset": "home",
"available": true,
"available_schedules": ["Weekschema", "Badkamer", "Test"],
"control_state": "off",
"dev_class": "zone_thermostat",
"firmware": "2016-10-10T02:00:00+02:00",
"hardware": "255",
@ -147,6 +172,7 @@
"cooling_present": false,
"gateway_id": "da224107914542988a88561b4452b0f6",
"heater_id": "056ee145a816487eaa69243c3280f8bf",
"item_count": 145,
"notifications": {},
"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_id": "cd3e822288064775a7c4afcdd70bdda2",
"item_count": 31,
"notifications": {},
"smile_name": "Smile P1"
}

View File

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

View File

@ -51,6 +51,7 @@
},
"gateway": {
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
"item_count": 40,
"notifications": {
"97a04c0c263049b29350a660b4cdd01e": {
"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_id": "0000aaaa0000aaaa0000aaaa0000aa00",
"item_count": 83,
"notifications": {},
"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,
'gateway_id': 'fe799307f1624099878210aa0b9f1475',
'heater_id': '90986d591dcd426cae3ec3e8111ff730',
'item_count': 315,
'notifications': 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.",

View File

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

View File

@ -5,6 +5,7 @@ from datetime import timedelta
from typing import Any
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from sqlalchemy import text as sql_text
from sqlalchemy.exc import SQLAlchemyError
@ -12,6 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError
from homeassistant.components.recorder import Recorder
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
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.const import (
CONF_ICON,
@ -21,6 +23,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
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.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 "device_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}}
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(
hass: HomeAssistant,

View File

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

View File

@ -8,6 +8,7 @@ import wave
from wyoming.asr import Transcribe, Transcript
from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.error import Error
from wyoming.event import Event
from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import RunSatellite
@ -96,6 +97,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient):
self.tts_audio_stop_event = asyncio.Event()
self.tts_audio_chunk: AudioChunk | None = None
self.error_event = asyncio.Event()
self.error: Error | None = None
self._mic_audio_chunk = AudioChunk(
rate=16000, width=2, channels=1, audio=b"chunk"
).event()
@ -135,6 +139,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient):
self.tts_audio_chunk_event.set()
elif AudioStop.is_type(event.type):
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:
"""Receive."""
@ -175,8 +182,9 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None:
await mock_client.connect_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"]
assert mock_run_pipeline.call_args.kwargs.get("device_id") == device.device_id
# Start detecting wake word
event_callback(
@ -458,3 +466,43 @@ async def test_satellite_disconnect_during_pipeline(hass: HomeAssistant) -> None
# Sensor should have been turned off
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):
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:
"""Test the object to JSON string filter."""

View File

@ -1,10 +1,12 @@
"""Test Home Assistant json utility functions."""
from pathlib import Path
import orjson
import pytest
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.json import (
json_loads,
json_loads_array,
json_loads_object,
load_json,
@ -153,3 +155,20 @@ async def test_deprecated_save_json(
save_json(fname, TEST_JSON_A)
assert "uses save_json from homeassistant.util.json" 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"