mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
2023.12.1 (#105324)
This commit is contained in:
commit
9b10af612a
@ -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:
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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]),
|
||||
]
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ DEFAULT_EXPOSED_DOMAINS = {
|
||||
"scene",
|
||||
"script",
|
||||
"switch",
|
||||
"todo",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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"]:
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,8 @@ class SQLSensor(ManualTriggerSensorEntity):
|
||||
self._query,
|
||||
redact_credentials(str(err)),
|
||||
)
|
||||
sess.rollback()
|
||||
sess.close()
|
||||
return
|
||||
|
||||
for res in result.mappings():
|
||||
|
@ -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",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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} == {
|
||||
|
@ -186,7 +186,6 @@ async def test_no_lights_or_groups(
|
||||
"state": STATE_ON,
|
||||
"attributes": {
|
||||
ATTR_EFFECT_LIST: [
|
||||
EFFECT_COLORLOOP,
|
||||
"carnival",
|
||||
"collide",
|
||||
"fading",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
[
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
|
13
tests/components/plugwise/fixtures/adam_jip/device_list.json
Normal file
13
tests/components/plugwise/fixtures/adam_jip/device_list.json
Normal file
@ -0,0 +1,13 @@
|
||||
[
|
||||
"b5c2386c6f6342669e50fe49dd05b188",
|
||||
"e4684553153b44afbef2200885f379dc",
|
||||
"a6abc6a129ee499c88a4d420cc413b47",
|
||||
"1346fbd8498d4dbcab7e18d51b771f3d",
|
||||
"833de10f269c4deab58fb9df69901b4e",
|
||||
"6f3e9d7084214c21b9dfa46f6eeb8700",
|
||||
"f61f1a2535f54f52ad006a3d18e459ca",
|
||||
"d4496250d0e942cfa7aea3476e9070d5",
|
||||
"356b65335e274d769c338223e7af9c33",
|
||||
"1da4d325838e4ad8aac12177214505c9",
|
||||
"457ce8414de24596a2d5e7dbc9c7682f"
|
||||
]
|
@ -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."
|
||||
|
@ -0,0 +1,20 @@
|
||||
[
|
||||
"fe799307f1624099878210aa0b9f1475",
|
||||
"90986d591dcd426cae3ec3e8111ff730",
|
||||
"df4a4a8169904cdb9c03d61a21f42140",
|
||||
"b310b72a0e354bfab43089919b9a88bf",
|
||||
"a2c3583e0a6349358998b760cea82d2a",
|
||||
"b59bcebaf94b499ea7d46e4a66fb62d8",
|
||||
"d3da73bde12a47d5a6b8f9dad971f2ec",
|
||||
"21f2b542c49845e6bb416884c55778d6",
|
||||
"78d1126fc4c743db81b61c20e88342a7",
|
||||
"cd0ddb54ef694e11ac18ed1cbce5dbbd",
|
||||
"4a810418d5394b3f82727340b91ba740",
|
||||
"02cf28bfec924855854c544690a609ef",
|
||||
"a28f588dc4a049a483fd03a30361ad3a",
|
||||
"6a3bf693d05e48e0b460c815a4fdd09d",
|
||||
"680423ff840043738f42cc7f1ff97a36",
|
||||
"f1fee6043d3642a9b0a65297455f008e",
|
||||
"675416a629f343c495449970e2ca37b5",
|
||||
"e7693eb9582644e5b865dba8d4447cf1"
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
"015ae9ea3f964e668e490fa39da3870b",
|
||||
"1cbf783bb11e4a7c8a6843dee3a86927",
|
||||
"3cb70739631c4d17a86b8b12e8a5161b"
|
||||
]
|
@ -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"
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
[
|
||||
"da224107914542988a88561b4452b0f6",
|
||||
"056ee145a816487eaa69243c3280f8bf",
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8",
|
||||
"1772a4ea304041adb83f357b751341ff",
|
||||
"e2f4322d57924fa090fbbc48b3a140dc",
|
||||
"e8ef2a01ed3b4139a53bf749204fe6b4"
|
||||
]
|
@ -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"
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
[
|
||||
"da224107914542988a88561b4452b0f6",
|
||||
"056ee145a816487eaa69243c3280f8bf",
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8",
|
||||
"1772a4ea304041adb83f357b751341ff",
|
||||
"e2f4322d57924fa090fbbc48b3a140dc",
|
||||
"e8ef2a01ed3b4139a53bf749204fe6b4"
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
"015ae9ea3f964e668e490fa39da3870b",
|
||||
"1cbf783bb11e4a7c8a6843dee3a86927",
|
||||
"3cb70739631c4d17a86b8b12e8a5161b"
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
"015ae9ea3f964e668e490fa39da3870b",
|
||||
"1cbf783bb11e4a7c8a6843dee3a86927",
|
||||
"3cb70739631c4d17a86b8b12e8a5161b"
|
||||
]
|
@ -42,6 +42,7 @@
|
||||
},
|
||||
"gateway": {
|
||||
"gateway_id": "cd3e822288064775a7c4afcdd70bdda2",
|
||||
"item_count": 31,
|
||||
"notifications": {},
|
||||
"smile_name": "Smile P1"
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
["cd3e822288064775a7c4afcdd70bdda2", "e950c7d5e1ee407a858e2a8b5016c8b3"]
|
@ -51,6 +51,7 @@
|
||||
},
|
||||
"gateway": {
|
||||
"gateway_id": "03e65b16e4b247a29ae0d75a78cb492e",
|
||||
"item_count": 40,
|
||||
"notifications": {
|
||||
"97a04c0c263049b29350a660b4cdd01e": {
|
||||
"warning": "The Smile P1 is not connected to a smart meter."
|
||||
|
@ -0,0 +1 @@
|
||||
["03e65b16e4b247a29ae0d75a78cb492e", "b82b6b3322484f2ea4e25e0bd5f3d61f"]
|
@ -135,6 +135,7 @@
|
||||
},
|
||||
"gateway": {
|
||||
"gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00",
|
||||
"item_count": 83,
|
||||
"notifications": {},
|
||||
"smile_name": "Stretch"
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
[
|
||||
"0000aaaa0000aaaa0000aaaa0000aa00",
|
||||
"5871317346d045bc9f6b987ef25ee638",
|
||||
"e1c884e7dede431dadee09506ec4f859",
|
||||
"aac7b735042c4832ac9ff33aae4f453b",
|
||||
"cfe95cf3de1948c0b8955125bf754614",
|
||||
"059e4d03c7a34d278add5c7a4a781d19",
|
||||
"d950b314e9d8499f968e6db8d82ef78c",
|
||||
"d03738edfcc947f7b8f4573571d90d2d"
|
||||
]
|
@ -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.",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -6,7 +6,7 @@
|
||||
'language': 'en',
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'transcibe',
|
||||
'type': 'transcribe',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
|
@ -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"
|
||||
|
@ -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."""
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user