mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
2023.12.2 (#105655)
This commit is contained in:
commit
711d9e21ab
@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
@ -51,25 +51,6 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Import configuration from yaml."""
|
||||
try:
|
||||
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
|
||||
except AbortFlow as err:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_already_configured",
|
||||
breaks_in_ha_version="2024.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_already_configured",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "AfterShip",
|
||||
},
|
||||
)
|
||||
raise err
|
||||
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
@ -84,6 +65,8 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"integration_title": "AfterShip",
|
||||
},
|
||||
)
|
||||
|
||||
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
|
||||
return self.async_create_entry(
|
||||
title=config.get(CONF_NAME, "AfterShip"),
|
||||
data={CONF_API_KEY: config[CONF_API_KEY]},
|
||||
|
@ -49,10 +49,6 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_already_configured": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant."
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
|
@ -1304,13 +1304,14 @@ async def async_api_set_range(
|
||||
service = None
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = directive.payload["rangeValue"]
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
if supported & cover.CoverEntityFeature.CLOSE and range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif range_value == 100:
|
||||
elif supported & cover.CoverEntityFeature.OPEN and range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
@ -1319,9 +1320,9 @@ async def async_api_set_range(
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
if supported & cover.CoverEntityFeature.CLOSE_TILT and range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
elif range_value == 100:
|
||||
elif supported & cover.CoverEntityFeature.OPEN_TILT and range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER_TILT
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||
@ -1332,13 +1333,11 @@ async def async_api_set_range(
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif supported & fan.FanEntityFeature.SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.FanEntityFeature.SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
service = fan.SERVICE_TURN_ON
|
||||
|
||||
# Humidifier target humidity
|
||||
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||
|
@ -369,6 +369,7 @@ class PipelineStage(StrEnum):
|
||||
STT = "stt"
|
||||
INTENT = "intent"
|
||||
TTS = "tts"
|
||||
END = "end"
|
||||
|
||||
|
||||
PIPELINE_STAGE_ORDER = [
|
||||
@ -1024,35 +1025,32 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
if tts_input := tts_input.strip():
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
tts_output = {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
else:
|
||||
tts_output = {}
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
tts_output = {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
|
||||
@ -1345,7 +1343,11 @@ class PipelineInput:
|
||||
self.conversation_id,
|
||||
self.device_id,
|
||||
)
|
||||
current_stage = PipelineStage.TTS
|
||||
if tts_input.strip():
|
||||
current_stage = PipelineStage.TTS
|
||||
else:
|
||||
# Skip TTS
|
||||
current_stage = PipelineStage.END
|
||||
|
||||
if self.run.end_stage != PipelineStage.INTENT:
|
||||
# text-to-speech
|
||||
|
@ -25,6 +25,11 @@ from .const import (
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
SERVICE_UPDATE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
)
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
@ -152,7 +157,7 @@ def setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
# Register all the above services
|
||||
service_mapping = [
|
||||
(blink_refresh, SERVICE_REFRESH, None),
|
||||
(blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA),
|
||||
(
|
||||
async_handle_save_video_service,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
|
@ -1,14 +1,28 @@
|
||||
# Describes the format for available Blink services
|
||||
|
||||
blink_update:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
|
||||
trigger_camera:
|
||||
target:
|
||||
entity:
|
||||
integration: blink
|
||||
domain: camera
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
|
||||
save_video:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
name:
|
||||
required: true
|
||||
example: "Living Room"
|
||||
@ -22,6 +36,11 @@ save_video:
|
||||
|
||||
save_recent_clips:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
name:
|
||||
required: true
|
||||
example: "Living Room"
|
||||
@ -35,6 +54,11 @@ save_recent_clips:
|
||||
|
||||
send_pin:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: blink
|
||||
pin:
|
||||
example: "abc123"
|
||||
selector:
|
||||
|
@ -57,11 +57,23 @@
|
||||
"services": {
|
||||
"blink_update": {
|
||||
"name": "Update",
|
||||
"description": "Forces a refresh."
|
||||
"description": "Forces a refresh.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
"trigger_camera": {
|
||||
"name": "Trigger camera",
|
||||
"description": "Requests camera to take new image."
|
||||
"description": "Requests camera to take new image.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
"save_video": {
|
||||
"name": "Save video",
|
||||
@ -74,6 +86,10 @@
|
||||
"filename": {
|
||||
"name": "File name",
|
||||
"description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -88,6 +104,10 @@
|
||||
"file_path": {
|
||||
"name": "Output directory",
|
||||
"description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -98,6 +118,10 @@
|
||||
"pin": {
|
||||
"name": "Pin",
|
||||
"description": "PIN received from blink. Leave empty if you only received a verification email."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The Blink device id."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.3.6"]
|
||||
"requirements": ["caldav==1.3.8"]
|
||||
}
|
||||
|
@ -98,10 +98,7 @@ def _to_ics_fields(item: TodoItem) -> dict[str, Any]:
|
||||
if status := item.status:
|
||||
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
||||
if due := item.due:
|
||||
if isinstance(due, datetime):
|
||||
item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ")
|
||||
else:
|
||||
item_data["due"] = due.strftime("%Y%m%d")
|
||||
item_data["due"] = due
|
||||
if description := item.description:
|
||||
item_data["description"] = description
|
||||
return item_data
|
||||
@ -162,7 +159,10 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||
vtodo.update(**_to_ics_fields(item))
|
||||
updated_fields = _to_ics_fields(item)
|
||||
if "due" in updated_fields:
|
||||
todo.set_due(updated_fields.pop("due")) # type: ignore[attr-defined]
|
||||
vtodo.update(**updated_fields)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
|
@ -21,7 +21,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
"""Handle GetTemperature intents."""
|
||||
|
||||
intent_type = INTENT_GET_TEMPERATURE
|
||||
slot_schema = {vol.Optional("area"): str}
|
||||
slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
@ -49,6 +49,20 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
elif "name" in slots:
|
||||
# Filter by name
|
||||
entity_name = slots["name"]["value"]
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity named {entity_name}")
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
else:
|
||||
# First entity
|
||||
|
@ -144,7 +144,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if not self._setup_complete:
|
||||
await self._async_setup_and_authenticate()
|
||||
self._async_mark_setup_complete()
|
||||
return (await envoy.update()).raw
|
||||
# dump all received data in debug mode to assist troubleshooting
|
||||
envoy_data = await envoy.update()
|
||||
_LOGGER.debug("Envoy data: %s", envoy_data)
|
||||
return envoy_data.raw
|
||||
except INVALID_AUTH_ERRORS as err:
|
||||
if self._setup_complete and tries == 0:
|
||||
# token likely expired or firmware changed, try to re-authenticate
|
||||
|
@ -35,15 +35,16 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Fast.com component. (deprecated)."""
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config[DOMAIN],
|
||||
if DOMAIN in config:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config[DOMAIN],
|
||||
)
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -60,7 +60,10 @@ class FitbitOAuth2Implementation(AuthImplementation):
|
||||
resp.raise_for_status()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
error_body = await resp.text() if not session.closed else ""
|
||||
try:
|
||||
error_body = await resp.text()
|
||||
except aiohttp.ClientError:
|
||||
error_body = ""
|
||||
_LOGGER.debug(
|
||||
"Client response error status=%s, body=%s", err.status, error_body
|
||||
)
|
||||
|
@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhiveapi==0.5.14"]
|
||||
"requirements": ["pyhiveapi==0.5.16"]
|
||||
}
|
||||
|
@ -405,7 +405,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
raise AbortFlow("characteristic_missing") from err
|
||||
except improv_ble_errors.CommandFailed:
|
||||
raise
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
raise AbortFlow("unknown") from err
|
||||
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==6.1.0"]
|
||||
"requirements": ["ical==6.1.1"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==6.1.0"]
|
||||
"requirements": ["ical==6.1.1"]
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
from time import localtime, strftime, time
|
||||
from typing import Any
|
||||
@ -151,6 +152,13 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class LyricThermostatType(enum.Enum):
|
||||
"""Lyric thermostats are classified as TCC or LCC devices."""
|
||||
|
||||
TCC = enum.auto()
|
||||
LCC = enum.auto()
|
||||
|
||||
|
||||
class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
"""Defines a Honeywell Lyric climate entity."""
|
||||
|
||||
@ -201,8 +209,10 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
# Setup supported features
|
||||
if device.changeableValues.thermostatSetpointStatus:
|
||||
self._attr_supported_features = SUPPORT_FLAGS_LCC
|
||||
self._attr_thermostat_type = LyricThermostatType.LCC
|
||||
else:
|
||||
self._attr_supported_features = SUPPORT_FLAGS_TCC
|
||||
self._attr_thermostat_type = LyricThermostatType.TCC
|
||||
|
||||
# Setup supported fan modes
|
||||
if device_fan_modes := device.settings.attributes.get("fan", {}).get(
|
||||
@ -365,56 +375,69 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
"""Set hvac mode."""
|
||||
_LOGGER.debug("HVAC mode: %s", hvac_mode)
|
||||
try:
|
||||
if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL:
|
||||
# If the system is off, turn it to Heat first then to Auto,
|
||||
# otherwise it turns to.
|
||||
# Auto briefly and then reverts to Off (perhaps related to
|
||||
# heatCoolMode). This is the behavior that happens with the
|
||||
# native app as well, so likely a bug in the api itself
|
||||
if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_COOL],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=False,
|
||||
)
|
||||
# Sleep 3 seconds before proceeding
|
||||
await asyncio.sleep(3)
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[self.device.changeableValues.mode],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, autoChangeoverActive=True
|
||||
)
|
||||
else:
|
||||
match self._attr_thermostat_type:
|
||||
case LyricThermostatType.TCC:
|
||||
await self._async_set_hvac_mode_tcc(hvac_mode)
|
||||
case LyricThermostatType.LCC:
|
||||
await self._async_set_hvac_mode_lcc(hvac_mode)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def _async_set_hvac_mode_tcc(self, hvac_mode: HVACMode) -> None:
|
||||
if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL:
|
||||
# If the system is off, turn it to Heat first then to Auto,
|
||||
# otherwise it turns to.
|
||||
# Auto briefly and then reverts to Off (perhaps related to
|
||||
# heatCoolMode). This is the behavior that happens with the
|
||||
# native app as well, so likely a bug in the api itself
|
||||
if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_COOL],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=False,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
await self.coordinator.async_refresh()
|
||||
# Sleep 3 seconds before proceeding
|
||||
await asyncio.sleep(3)
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"HVAC mode passed to lyric: %s",
|
||||
HVAC_MODES[self.device.changeableValues.mode],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, autoChangeoverActive=True
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=False,
|
||||
)
|
||||
|
||||
async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None:
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode."""
|
||||
|
@ -27,6 +27,7 @@ async def async_setup_entry(
|
||||
Alpha2IODeviceBatterySensor(coordinator, io_device_id)
|
||||
for io_device_id, io_device in coordinator.data["io_devices"].items()
|
||||
if io_device["_HEATAREA_ID"]
|
||||
and io_device["_HEATAREA_ID"] in coordinator.data["heat_areas"]
|
||||
)
|
||||
|
||||
|
||||
|
@ -25,7 +25,7 @@ async def async_setup_entry(
|
||||
Alpha2HeatControlValveOpeningSensor(coordinator, heat_control_id)
|
||||
for heat_control_id, heat_control in coordinator.data["heat_controls"].items()
|
||||
if heat_control["INUSE"]
|
||||
and heat_control["_HEATAREA_ID"]
|
||||
and heat_control["_HEATAREA_ID"] in coordinator.data["heat_areas"]
|
||||
and heat_control.get("ACTOR_PERCENT") is not None
|
||||
)
|
||||
|
||||
|
@ -247,7 +247,6 @@ async def async_check_config_schema(
|
||||
schema(config)
|
||||
except vol.Invalid as exc:
|
||||
integration = await async_get_integration(hass, DOMAIN)
|
||||
# pylint: disable-next=protected-access
|
||||
message = conf_util.format_schema_error(
|
||||
hass, exc, domain, config, integration.documentation
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
from typing import Final, Literal
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@ -36,6 +36,23 @@ ZEROCONF_MAP: Final[dict[str, str]] = {
|
||||
"stretch": "Stretch",
|
||||
}
|
||||
|
||||
NumberType = Literal[
|
||||
"maximum_boiler_temperature",
|
||||
"max_dhw_temperature",
|
||||
"temperature_offset",
|
||||
]
|
||||
|
||||
SelectType = Literal[
|
||||
"select_dhw_mode",
|
||||
"select_regulation_mode",
|
||||
"select_schedule",
|
||||
]
|
||||
SelectOptionsType = Literal[
|
||||
"dhw_modes",
|
||||
"regulation_modes",
|
||||
"available_schedules",
|
||||
]
|
||||
|
||||
# Default directives
|
||||
DEFAULT_MAX_TEMP: Final = 30
|
||||
DEFAULT_MIN_TEMP: Final = 4
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["crcmod", "plugwise"],
|
||||
"requirements": ["plugwise==0.34.5"],
|
||||
"requirements": ["plugwise==0.35.3"],
|
||||
"zeroconf": ["_plugwise._tcp.local."]
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from plugwise import Smile
|
||||
from plugwise.constants import NumberType
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
@ -18,7 +17,7 @@ from homeassistant.const import EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, NumberType
|
||||
from .coordinator import PlugwiseDataUpdateCoordinator
|
||||
from .entity import PlugwiseEntity
|
||||
|
||||
|
@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from plugwise import Smile
|
||||
from plugwise.constants import SelectOptionsType, SelectType
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -13,7 +12,7 @@ from homeassistant.const import STATE_ON, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SelectOptionsType, SelectType
|
||||
from .coordinator import PlugwiseDataUpdateCoordinator
|
||||
from .entity import PlugwiseEntity
|
||||
|
||||
|
@ -108,7 +108,10 @@
|
||||
}
|
||||
},
|
||||
"select_schedule": {
|
||||
"name": "Thermostat schedule"
|
||||
"name": "Thermostat schedule",
|
||||
"state": {
|
||||
"off": "Off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyschlage==2023.11.0"]
|
||||
"requirements": ["pyschlage==2023.12.0"]
|
||||
}
|
||||
|
@ -528,10 +528,10 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
|
||||
def _determine_preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes."""
|
||||
supported_modes = self._device.status.attributes[
|
||||
supported_modes: list | None = self._device.status.attributes[
|
||||
"supportedAcOptionalMode"
|
||||
].value
|
||||
if WINDFREE in supported_modes:
|
||||
if supported_modes and WINDFREE in supported_modes:
|
||||
return [WINDFREE]
|
||||
return None
|
||||
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["hatasmota"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"requirements": ["HATasmota==0.7.3"]
|
||||
"requirements": ["HATasmota==0.8.0"]
|
||||
}
|
||||
|
@ -112,8 +112,11 @@ class TasmotaAvailability(TasmotaEntity):
|
||||
|
||||
def __init__(self, **kwds: Any) -> None:
|
||||
"""Initialize the availability mixin."""
|
||||
self._available = False
|
||||
super().__init__(**kwds)
|
||||
if self._tasmota_entity.deep_sleep_enabled:
|
||||
self._available = True
|
||||
else:
|
||||
self._available = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to MQTT events."""
|
||||
@ -122,6 +125,8 @@ class TasmotaAvailability(TasmotaEntity):
|
||||
async_subscribe_connection_status(self.hass, self.async_mqtt_connected)
|
||||
)
|
||||
await super().async_added_to_hass()
|
||||
if self._tasmota_entity.deep_sleep_enabled:
|
||||
await self._tasmota_entity.poll_status()
|
||||
|
||||
async def availability_updated(self, available: bool) -> None:
|
||||
"""Handle updated availability."""
|
||||
@ -135,6 +140,8 @@ class TasmotaAvailability(TasmotaEntity):
|
||||
if not self.hass.is_stopping:
|
||||
if not mqtt_connected(self.hass):
|
||||
self._available = False
|
||||
elif self._tasmota_entity.deep_sleep_enabled:
|
||||
self._available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -192,52 +192,67 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = withings_data
|
||||
|
||||
register_lock = asyncio.Lock()
|
||||
webhooks_registered = False
|
||||
|
||||
async def unregister_webhook(
|
||||
_: Any,
|
||||
) -> None:
|
||||
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
|
||||
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
await async_unsubscribe_webhooks(client)
|
||||
for coordinator in withings_data.coordinators:
|
||||
coordinator.webhook_subscription_listener(False)
|
||||
nonlocal webhooks_registered
|
||||
async with register_lock:
|
||||
LOGGER.debug(
|
||||
"Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]
|
||||
)
|
||||
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
await async_unsubscribe_webhooks(client)
|
||||
for coordinator in withings_data.coordinators:
|
||||
coordinator.webhook_subscription_listener(False)
|
||||
webhooks_registered = False
|
||||
|
||||
async def register_webhook(
|
||||
_: Any,
|
||||
) -> None:
|
||||
if cloud.async_active_subscription(hass):
|
||||
webhook_url = await _async_cloudhook_generate_url(hass, entry)
|
||||
else:
|
||||
webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
url = URL(webhook_url)
|
||||
if url.scheme != "https" or url.port != 443:
|
||||
LOGGER.warning(
|
||||
"Webhook not registered - "
|
||||
"https and port 443 is required to register the webhook"
|
||||
nonlocal webhooks_registered
|
||||
async with register_lock:
|
||||
if webhooks_registered:
|
||||
return
|
||||
if cloud.async_active_subscription(hass):
|
||||
webhook_url = await _async_cloudhook_generate_url(hass, entry)
|
||||
else:
|
||||
webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
url = URL(webhook_url)
|
||||
if url.scheme != "https" or url.port != 443:
|
||||
LOGGER.warning(
|
||||
"Webhook not registered - "
|
||||
"https and port 443 is required to register the webhook"
|
||||
)
|
||||
return
|
||||
|
||||
webhook_name = "Withings"
|
||||
if entry.title != DEFAULT_TITLE:
|
||||
webhook_name = f"{DEFAULT_TITLE} {entry.title}"
|
||||
|
||||
webhook_register(
|
||||
hass,
|
||||
DOMAIN,
|
||||
webhook_name,
|
||||
entry.data[CONF_WEBHOOK_ID],
|
||||
get_webhook_handler(withings_data),
|
||||
allowed_methods=[METH_POST],
|
||||
)
|
||||
return
|
||||
LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url)
|
||||
|
||||
webhook_name = "Withings"
|
||||
if entry.title != DEFAULT_TITLE:
|
||||
webhook_name = f"{DEFAULT_TITLE} {entry.title}"
|
||||
|
||||
webhook_register(
|
||||
hass,
|
||||
DOMAIN,
|
||||
webhook_name,
|
||||
entry.data[CONF_WEBHOOK_ID],
|
||||
get_webhook_handler(withings_data),
|
||||
allowed_methods=[METH_POST],
|
||||
)
|
||||
|
||||
await async_subscribe_webhooks(client, webhook_url)
|
||||
for coordinator in withings_data.coordinators:
|
||||
coordinator.webhook_subscription_listener(True)
|
||||
LOGGER.debug("Register Withings webhook: %s", webhook_url)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
|
||||
)
|
||||
await async_subscribe_webhooks(client, webhook_url)
|
||||
for coordinator in withings_data.coordinators:
|
||||
coordinator.webhook_subscription_listener(True)
|
||||
LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
|
||||
)
|
||||
webhooks_registered = True
|
||||
|
||||
async def manage_cloudhook(state: cloud.CloudConnectionState) -> None:
|
||||
LOGGER.debug("Cloudconnection state changed to %s", state)
|
||||
if state is cloud.CloudConnectionState.CLOUD_CONNECTED:
|
||||
await register_webhook(None)
|
||||
|
||||
|
@ -17,11 +17,11 @@ class SatelliteDevice:
|
||||
satellite_id: str
|
||||
device_id: str
|
||||
is_active: bool = False
|
||||
is_enabled: bool = True
|
||||
is_muted: bool = False
|
||||
pipeline_name: str | None = None
|
||||
|
||||
_is_active_listener: Callable[[], None] | None = None
|
||||
_is_enabled_listener: Callable[[], None] | None = None
|
||||
_is_muted_listener: Callable[[], None] | None = None
|
||||
_pipeline_listener: Callable[[], None] | None = None
|
||||
|
||||
@callback
|
||||
@ -33,12 +33,12 @@ class SatelliteDevice:
|
||||
self._is_active_listener()
|
||||
|
||||
@callback
|
||||
def set_is_enabled(self, enabled: bool) -> None:
|
||||
"""Set enabled state."""
|
||||
if enabled != self.is_enabled:
|
||||
self.is_enabled = enabled
|
||||
if self._is_enabled_listener is not None:
|
||||
self._is_enabled_listener()
|
||||
def set_is_muted(self, muted: bool) -> None:
|
||||
"""Set muted state."""
|
||||
if muted != self.is_muted:
|
||||
self.is_muted = muted
|
||||
if self._is_muted_listener is not None:
|
||||
self._is_muted_listener()
|
||||
|
||||
@callback
|
||||
def set_pipeline_name(self, pipeline_name: str) -> None:
|
||||
@ -54,9 +54,9 @@ class SatelliteDevice:
|
||||
self._is_active_listener = is_active_listener
|
||||
|
||||
@callback
|
||||
def set_is_enabled_listener(self, is_enabled_listener: Callable[[], None]) -> None:
|
||||
"""Listen for updates to is_enabled."""
|
||||
self._is_enabled_listener = is_enabled_listener
|
||||
def set_is_muted_listener(self, is_muted_listener: Callable[[], None]) -> None:
|
||||
"""Listen for updates to muted status."""
|
||||
self._is_muted_listener = is_muted_listener
|
||||
|
||||
@callback
|
||||
def set_pipeline_listener(self, pipeline_listener: Callable[[], None]) -> None:
|
||||
@ -70,11 +70,11 @@ class SatelliteDevice:
|
||||
"binary_sensor", DOMAIN, f"{self.satellite_id}-assist_in_progress"
|
||||
)
|
||||
|
||||
def get_satellite_enabled_entity_id(self, hass: HomeAssistant) -> str | None:
|
||||
"""Return entity id for satellite enabled switch."""
|
||||
def get_muted_entity_id(self, hass: HomeAssistant) -> str | None:
|
||||
"""Return entity id for satellite muted switch."""
|
||||
ent_reg = er.async_get(hass)
|
||||
return ent_reg.async_get_entity_id(
|
||||
"switch", DOMAIN, f"{self.satellite_id}-satellite_enabled"
|
||||
"switch", DOMAIN, f"{self.satellite_id}-mute"
|
||||
)
|
||||
|
||||
def get_pipeline_entity_id(self, hass: HomeAssistant) -> str | None:
|
||||
|
@ -49,7 +49,6 @@ class WyomingSatellite:
|
||||
self.hass = hass
|
||||
self.service = service
|
||||
self.device = device
|
||||
self.is_enabled = True
|
||||
self.is_running = True
|
||||
|
||||
self._client: AsyncTcpClient | None = None
|
||||
@ -57,9 +56,9 @@ class WyomingSatellite:
|
||||
self._is_pipeline_running = False
|
||||
self._audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
|
||||
self._pipeline_id: str | None = None
|
||||
self._enabled_changed_event = asyncio.Event()
|
||||
self._muted_changed_event = asyncio.Event()
|
||||
|
||||
self.device.set_is_enabled_listener(self._enabled_changed)
|
||||
self.device.set_is_muted_listener(self._muted_changed)
|
||||
self.device.set_pipeline_listener(self._pipeline_changed)
|
||||
|
||||
async def run(self) -> None:
|
||||
@ -69,12 +68,12 @@ class WyomingSatellite:
|
||||
try:
|
||||
while self.is_running:
|
||||
try:
|
||||
# Check if satellite has been disabled
|
||||
if not self.device.is_enabled:
|
||||
await self.on_disabled()
|
||||
# Check if satellite has been muted
|
||||
while self.device.is_muted:
|
||||
await self.on_muted()
|
||||
if not self.is_running:
|
||||
# Satellite was stopped while waiting to be enabled
|
||||
break
|
||||
# Satellite was stopped while waiting to be unmuted
|
||||
return
|
||||
|
||||
# Connect and run pipeline loop
|
||||
await self._run_once()
|
||||
@ -86,14 +85,14 @@ class WyomingSatellite:
|
||||
# Ensure sensor is off
|
||||
self.device.set_is_active(False)
|
||||
|
||||
await self.on_stopped()
|
||||
await self.on_stopped()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Signal satellite task to stop running."""
|
||||
self.is_running = False
|
||||
|
||||
# Unblock waiting for enabled
|
||||
self._enabled_changed_event.set()
|
||||
# Unblock waiting for unmuted
|
||||
self._muted_changed_event.set()
|
||||
|
||||
async def on_restart(self) -> None:
|
||||
"""Block until pipeline loop will be restarted."""
|
||||
@ -111,9 +110,9 @@ class WyomingSatellite:
|
||||
)
|
||||
await asyncio.sleep(_RECONNECT_SECONDS)
|
||||
|
||||
async def on_disabled(self) -> None:
|
||||
"""Block until device may be enabled again."""
|
||||
await self._enabled_changed_event.wait()
|
||||
async def on_muted(self) -> None:
|
||||
"""Block until device may be unmated again."""
|
||||
await self._muted_changed_event.wait()
|
||||
|
||||
async def on_stopped(self) -> None:
|
||||
"""Run when run() has fully stopped."""
|
||||
@ -121,14 +120,14 @@ class WyomingSatellite:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _enabled_changed(self) -> None:
|
||||
"""Run when device enabled status changes."""
|
||||
|
||||
if not self.device.is_enabled:
|
||||
def _muted_changed(self) -> None:
|
||||
"""Run when device muted status changes."""
|
||||
if self.device.is_muted:
|
||||
# Cancel any running pipeline
|
||||
self._audio_queue.put_nowait(None)
|
||||
|
||||
self._enabled_changed_event.set()
|
||||
self._muted_changed_event.set()
|
||||
self._muted_changed_event.clear()
|
||||
|
||||
def _pipeline_changed(self) -> None:
|
||||
"""Run when device pipeline changes."""
|
||||
@ -140,7 +139,7 @@ class WyomingSatellite:
|
||||
"""Run pipelines until an error occurs."""
|
||||
self.device.set_is_active(False)
|
||||
|
||||
while self.is_running and self.is_enabled:
|
||||
while self.is_running and (not self.device.is_muted):
|
||||
try:
|
||||
await self._connect()
|
||||
break
|
||||
@ -150,7 +149,7 @@ class WyomingSatellite:
|
||||
assert self._client is not None
|
||||
_LOGGER.debug("Connected to satellite")
|
||||
|
||||
if (not self.is_running) or (not self.is_enabled):
|
||||
if (not self.is_running) or self.device.is_muted:
|
||||
# Run was cancelled or satellite was disabled during connection
|
||||
return
|
||||
|
||||
@ -159,7 +158,7 @@ class WyomingSatellite:
|
||||
|
||||
# Wait until we get RunPipeline event
|
||||
run_pipeline: RunPipeline | None = None
|
||||
while self.is_running and self.is_enabled:
|
||||
while self.is_running and (not self.device.is_muted):
|
||||
run_event = await self._client.read_event()
|
||||
if run_event is None:
|
||||
raise ConnectionResetError("Satellite disconnected")
|
||||
@ -173,7 +172,7 @@ class WyomingSatellite:
|
||||
assert run_pipeline is not None
|
||||
_LOGGER.debug("Received run information: %s", run_pipeline)
|
||||
|
||||
if (not self.is_running) or (not self.is_enabled):
|
||||
if (not self.is_running) or self.device.is_muted:
|
||||
# Run was cancelled or satellite was disabled while waiting for
|
||||
# RunPipeline event.
|
||||
return
|
||||
@ -188,7 +187,7 @@ class WyomingSatellite:
|
||||
raise ValueError(f"Invalid end stage: {end_stage}")
|
||||
|
||||
# Each loop is a pipeline run
|
||||
while self.is_running and self.is_enabled:
|
||||
while self.is_running and (not self.device.is_muted):
|
||||
# Use select to get pipeline each time in case it's changed
|
||||
pipeline_id = pipeline_select.get_chosen_pipeline(
|
||||
self.hass,
|
||||
@ -243,9 +242,17 @@ class WyomingSatellite:
|
||||
chunk = AudioChunk.from_event(client_event)
|
||||
chunk = self._chunk_converter.convert(chunk)
|
||||
self._audio_queue.put_nowait(chunk.audio)
|
||||
elif AudioStop.is_type(client_event.type):
|
||||
# Stop pipeline
|
||||
_LOGGER.debug("Client requested pipeline to stop")
|
||||
self._audio_queue.put_nowait(b"")
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Unexpected event from satellite: %s", client_event)
|
||||
|
||||
# Ensure task finishes
|
||||
await _pipeline_task
|
||||
|
||||
_LOGGER.debug("Pipeline finished")
|
||||
|
||||
def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None:
|
||||
@ -336,12 +343,23 @@ class WyomingSatellite:
|
||||
|
||||
async def _connect(self) -> None:
|
||||
"""Connect to satellite over TCP."""
|
||||
await self._disconnect()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Connecting to satellite at %s:%s", self.service.host, self.service.port
|
||||
)
|
||||
self._client = AsyncTcpClient(self.service.host, self.service.port)
|
||||
await self._client.connect()
|
||||
|
||||
async def _disconnect(self) -> None:
|
||||
"""Disconnect if satellite is currently connected."""
|
||||
if self._client is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Disconnecting from satellite")
|
||||
await self._client.disconnect()
|
||||
self._client = None
|
||||
|
||||
async def _stream_tts(self, media_id: str) -> None:
|
||||
"""Stream TTS WAV audio to satellite in chunks."""
|
||||
assert self._client is not None
|
||||
|
@ -42,8 +42,8 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"satellite_enabled": {
|
||||
"name": "Satellite enabled"
|
||||
"mute": {
|
||||
"name": "Mute"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,17 +29,17 @@ async def async_setup_entry(
|
||||
# Setup is only forwarded for satellites
|
||||
assert item.satellite is not None
|
||||
|
||||
async_add_entities([WyomingSatelliteEnabledSwitch(item.satellite.device)])
|
||||
async_add_entities([WyomingSatelliteMuteSwitch(item.satellite.device)])
|
||||
|
||||
|
||||
class WyomingSatelliteEnabledSwitch(
|
||||
class WyomingSatelliteMuteSwitch(
|
||||
WyomingSatelliteEntity, restore_state.RestoreEntity, SwitchEntity
|
||||
):
|
||||
"""Entity to represent if satellite is enabled."""
|
||||
"""Entity to represent if satellite is muted."""
|
||||
|
||||
entity_description = SwitchEntityDescription(
|
||||
key="satellite_enabled",
|
||||
translation_key="satellite_enabled",
|
||||
key="mute",
|
||||
translation_key="mute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
@ -49,17 +49,17 @@ class WyomingSatelliteEnabledSwitch(
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
|
||||
# Default to on
|
||||
self._attr_is_on = (state is None) or (state.state == STATE_ON)
|
||||
# Default to off
|
||||
self._attr_is_on = (state is not None) and (state.state == STATE_ON)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
self._device.set_is_enabled(True)
|
||||
self._device.set_is_muted(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
self._device.set_is_enabled(False)
|
||||
self._device.set_is_muted(False)
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.127.0"]
|
||||
"requirements": ["zeroconf==0.128.4"]
|
||||
}
|
||||
|
@ -37,8 +37,6 @@ from .core.const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
STARTUP_FAILURE_DELAY_S,
|
||||
STARTUP_RETRIES,
|
||||
RadioType,
|
||||
)
|
||||
from .core.device import get_device_automation_triggers
|
||||
@ -161,49 +159,40 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
_LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache)
|
||||
|
||||
# Retry setup a few times before giving up to deal with missing serial ports in VMs
|
||||
for attempt in range(STARTUP_RETRIES):
|
||||
try:
|
||||
zha_gateway = await ZHAGateway.async_from_config(
|
||||
hass=hass,
|
||||
config=zha_data.yaml_config,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
break
|
||||
except NetworkSettingsInconsistent as exc:
|
||||
await warn_on_inconsistent_network_settings(
|
||||
hass,
|
||||
config_entry=config_entry,
|
||||
old_state=exc.old_state,
|
||||
new_state=exc.new_state,
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
"Network settings do not match most recent backup"
|
||||
) from exc
|
||||
except TransientConnectionError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
_LOGGER.debug(
|
||||
"Couldn't start coordinator (attempt %s of %s)",
|
||||
attempt + 1,
|
||||
STARTUP_RETRIES,
|
||||
exc_info=exc,
|
||||
)
|
||||
try:
|
||||
zha_gateway = await ZHAGateway.async_from_config(
|
||||
hass=hass,
|
||||
config=zha_data.yaml_config,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
except NetworkSettingsInconsistent as exc:
|
||||
await warn_on_inconsistent_network_settings(
|
||||
hass,
|
||||
config_entry=config_entry,
|
||||
old_state=exc.old_state,
|
||||
new_state=exc.new_state,
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
"Network settings do not match most recent backup"
|
||||
) from exc
|
||||
except TransientConnectionError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc:
|
||||
_LOGGER.debug("Failed to set up ZHA", exc_info=exc)
|
||||
device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
|
||||
if attempt < STARTUP_RETRIES - 1:
|
||||
await asyncio.sleep(STARTUP_FAILURE_DELAY_S)
|
||||
continue
|
||||
if (
|
||||
not device_path.startswith("socket://")
|
||||
and RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp
|
||||
):
|
||||
try:
|
||||
# Ignore all exceptions during probing, they shouldn't halt setup
|
||||
if await warn_on_wrong_silabs_firmware(hass, device_path):
|
||||
raise ConfigEntryError("Incorrect firmware installed") from exc
|
||||
except AlreadyRunningEZSP as ezsp_exc:
|
||||
raise ConfigEntryNotReady from ezsp_exc
|
||||
|
||||
if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp:
|
||||
try:
|
||||
# Ignore all exceptions during probing, they shouldn't halt setup
|
||||
await warn_on_wrong_silabs_firmware(
|
||||
hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
)
|
||||
except AlreadyRunningEZSP as ezsp_exc:
|
||||
raise ConfigEntryNotReady from ezsp_exc
|
||||
|
||||
raise
|
||||
raise ConfigEntryNotReady from exc
|
||||
|
||||
repairs.async_delete_blocking_issues(hass)
|
||||
|
||||
|
@ -139,7 +139,6 @@ CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join"
|
||||
CONF_ENABLE_QUIRKS = "enable_quirks"
|
||||
CONF_RADIO_TYPE = "radio_type"
|
||||
CONF_USB_PATH = "usb_path"
|
||||
CONF_USE_THREAD = "use_thread"
|
||||
CONF_ZIGPY = "zigpy_config"
|
||||
|
||||
CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains"
|
||||
@ -409,9 +408,6 @@ class Strobe(t.enum8):
|
||||
Strobe = 0x01
|
||||
|
||||
|
||||
STARTUP_FAILURE_DELAY_S = 3
|
||||
STARTUP_RETRIES = 3
|
||||
|
||||
EZSP_OVERWRITE_EUI64 = (
|
||||
"i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"
|
||||
)
|
||||
|
@ -46,7 +46,6 @@ from .const import (
|
||||
ATTR_SIGNATURE,
|
||||
ATTR_TYPE,
|
||||
CONF_RADIO_TYPE,
|
||||
CONF_USE_THREAD,
|
||||
CONF_ZIGPY,
|
||||
DEBUG_COMP_BELLOWS,
|
||||
DEBUG_COMP_ZHA,
|
||||
@ -158,15 +157,6 @@ class ZHAGateway:
|
||||
if CONF_NWK_VALIDATE_SETTINGS not in app_config:
|
||||
app_config[CONF_NWK_VALIDATE_SETTINGS] = True
|
||||
|
||||
# The bellows UART thread sometimes propagates a cancellation into the main Core
|
||||
# event loop, when a connection to a TCP coordinator fails in a specific way
|
||||
if (
|
||||
CONF_USE_THREAD not in app_config
|
||||
and radio_type is RadioType.ezsp
|
||||
and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://")
|
||||
):
|
||||
app_config[CONF_USE_THREAD] = False
|
||||
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
|
@ -21,13 +21,13 @@
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.37.1",
|
||||
"bellows==0.37.3",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.107",
|
||||
"zigpy-deconz==0.22.0",
|
||||
"zigpy==0.60.0",
|
||||
"zigpy-xbee==0.20.0",
|
||||
"zigpy-deconz==0.22.2",
|
||||
"zigpy==0.60.1",
|
||||
"zigpy-xbee==0.20.1",
|
||||
"zigpy-zigate==0.12.0",
|
||||
"zigpy-znp==0.12.0",
|
||||
"universal-silabs-flasher==0.0.15",
|
||||
|
@ -10,7 +10,6 @@ import logging
|
||||
import os
|
||||
from typing import Any, Self
|
||||
|
||||
from bellows.config import CONF_USE_THREAD
|
||||
import voluptuous as vol
|
||||
from zigpy.application import ControllerApplication
|
||||
import zigpy.backups
|
||||
@ -175,7 +174,6 @@ class ZhaRadioManager:
|
||||
app_config[CONF_DATABASE] = database_path
|
||||
app_config[CONF_DEVICE] = self.device_settings
|
||||
app_config[CONF_NWK_BACKUP_ENABLED] = False
|
||||
app_config[CONF_USE_THREAD] = False
|
||||
app_config = self.radio_type.controller.SCHEMA(app_config)
|
||||
|
||||
app = await self.radio_type.controller.new(
|
||||
|
@ -7,7 +7,7 @@ from typing import Final
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__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)
|
||||
|
@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous==0.13.1
|
||||
webrtc-noise-gain==1.2.3
|
||||
yarl==1.9.2
|
||||
zeroconf==0.127.0
|
||||
zeroconf==0.128.4
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.12.1"
|
||||
version = "2023.12.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -28,7 +28,7 @@ DoorBirdPy==2.1.0
|
||||
HAP-python==4.9.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
HATasmota==0.7.3
|
||||
HATasmota==0.8.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
@ -523,7 +523,7 @@ beautifulsoup4==4.12.2
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.37.1
|
||||
bellows==0.37.3
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.14.6
|
||||
@ -604,7 +604,7 @@ btsmarthub-devicelist==0.2.3
|
||||
buienradar==1.0.5
|
||||
|
||||
# homeassistant.components.caldav
|
||||
caldav==1.3.6
|
||||
caldav==1.3.8
|
||||
|
||||
# homeassistant.components.circuit
|
||||
circuit-webhook==1.0.1
|
||||
@ -1054,7 +1054,7 @@ ibmiotf==0.3.4
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
ical==6.1.0
|
||||
ical==6.1.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@ -1476,7 +1476,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.14
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.34.5
|
||||
plugwise==0.35.3
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@ -1775,7 +1775,7 @@ pyhaversion==22.8.0
|
||||
pyheos==0.7.2
|
||||
|
||||
# homeassistant.components.hive
|
||||
pyhiveapi==0.5.14
|
||||
pyhiveapi==0.5.16
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.77
|
||||
@ -2026,7 +2026,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2023.11.0
|
||||
pyschlage==2023.12.0
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.36
|
||||
@ -2810,7 +2810,7 @@ zamg==0.3.3
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.127.0
|
||||
zeroconf==0.128.4
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
@ -2825,10 +2825,10 @@ zhong-hong-hvac==1.0.9
|
||||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.22.0
|
||||
zigpy-deconz==0.22.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.20.0
|
||||
zigpy-xbee==0.20.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.12.0
|
||||
@ -2837,7 +2837,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.60.0
|
||||
zigpy==0.60.1
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
@ -14,7 +14,7 @@ mock-open==1.4.0
|
||||
mypy==1.7.1
|
||||
pre-commit==3.5.0
|
||||
pydantic==1.10.12
|
||||
pylint==3.0.2
|
||||
pylint==3.0.3
|
||||
pylint-per-file-ignores==1.2.1
|
||||
pipdeptree==2.11.0
|
||||
pytest-asyncio==0.21.0
|
||||
|
@ -25,7 +25,7 @@ DoorBirdPy==2.1.0
|
||||
HAP-python==4.9.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
HATasmota==0.7.3
|
||||
HATasmota==0.8.0
|
||||
|
||||
# homeassistant.components.doods
|
||||
# homeassistant.components.generic
|
||||
@ -445,7 +445,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.12.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.37.1
|
||||
bellows==0.37.3
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.14.6
|
||||
@ -503,7 +503,7 @@ bthome-ble==3.2.0
|
||||
buienradar==1.0.5
|
||||
|
||||
# homeassistant.components.caldav
|
||||
caldav==1.3.6
|
||||
caldav==1.3.8
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase==2.1.0
|
||||
@ -832,7 +832,7 @@ ibeacon-ble==1.0.1
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
ical==6.1.0
|
||||
ical==6.1.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@ -1134,7 +1134,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.14
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.34.5
|
||||
plugwise==0.35.3
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@ -1340,7 +1340,7 @@ pyhaversion==22.8.0
|
||||
pyheos==0.7.2
|
||||
|
||||
# homeassistant.components.hive
|
||||
pyhiveapi==0.5.14
|
||||
pyhiveapi==0.5.16
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.77
|
||||
@ -1531,7 +1531,7 @@ pyrympro==0.0.7
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2023.11.0
|
||||
pyschlage==2023.12.0
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.36
|
||||
@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16
|
||||
zamg==0.3.3
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.127.0
|
||||
zeroconf==0.128.4
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
@ -2114,10 +2114,10 @@ zeversolar==0.3.1
|
||||
zha-quirks==0.0.107
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.22.0
|
||||
zigpy-deconz==0.22.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.20.0
|
||||
zigpy-xbee==0.20.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.12.0
|
||||
@ -2126,7 +2126,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.60.0
|
||||
zigpy==0.60.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.54.0
|
||||
|
@ -77,7 +77,9 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non
|
||||
}
|
||||
|
||||
|
||||
async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None:
|
||||
async def test_import_flow(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_setup_entry
|
||||
) -> None:
|
||||
"""Test importing yaml config."""
|
||||
|
||||
with patch(
|
||||
@ -95,11 +97,12 @@ async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "yaml-api-key",
|
||||
}
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_import_flow_already_exists(hass: HomeAssistant) -> None:
|
||||
async def test_import_flow_already_exists(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test importing yaml config where entry already exists."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "yaml-api-key"})
|
||||
entry.add_to_hass(hass)
|
||||
@ -108,3 +111,4 @@ async def test_import_flow_already_exists(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.alexa import smart_home, state_report
|
||||
import homeassistant.components.camera as camera
|
||||
from homeassistant.components.cover import CoverDeviceClass
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.components.vacuum import VacuumEntityFeature
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
@ -1884,8 +1884,199 @@ async def test_group(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_cover_position_range(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("position", "position_attr_in_service_call", "supported_features", "service_call"),
|
||||
[
|
||||
(
|
||||
30,
|
||||
30,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
0,
|
||||
None,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE,
|
||||
"cover.close_cover",
|
||||
),
|
||||
(
|
||||
99,
|
||||
99,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
None,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE,
|
||||
"cover.open_cover",
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
CoverEntityFeature.SET_POSITION,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
60,
|
||||
60,
|
||||
CoverEntityFeature.SET_POSITION,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
100,
|
||||
CoverEntityFeature.SET_POSITION,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
100,
|
||||
CoverEntityFeature.SET_POSITION | CoverEntityFeature.CLOSE,
|
||||
"cover.set_cover_position",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"position_30_open_close",
|
||||
"position_0_open_close",
|
||||
"position_99_open_close",
|
||||
"position_100_open_close",
|
||||
"position_0_no_open_close",
|
||||
"position_60_no_open_close",
|
||||
"position_100_no_open_close",
|
||||
"position_0_no_close",
|
||||
"position_100_no_open",
|
||||
],
|
||||
)
|
||||
async def test_cover_position(
|
||||
hass: HomeAssistant,
|
||||
position: int,
|
||||
position_attr_in_service_call: int | None,
|
||||
supported_features: CoverEntityFeature,
|
||||
service_call: str,
|
||||
) -> None:
|
||||
"""Test cover discovery and position using rangeController."""
|
||||
device = (
|
||||
"cover.test_range",
|
||||
"open",
|
||||
{
|
||||
"friendly_name": "Test cover range",
|
||||
"device_class": "blind",
|
||||
"supported_features": supported_features,
|
||||
"position": position,
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "cover#test_range"
|
||||
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||
assert appliance["friendlyName"] == "Test cover range"
|
||||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PowerController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "cover.position"
|
||||
|
||||
properties = range_capability["properties"]
|
||||
assert properties["nonControllable"] is False
|
||||
assert {"name": "rangeValue"} in properties["supported"]
|
||||
|
||||
capability_resources = range_capability["capabilityResources"]
|
||||
assert capability_resources is not None
|
||||
assert {
|
||||
"@type": "text",
|
||||
"value": {"text": "Position", "locale": "en-US"},
|
||||
} in capability_resources["friendlyNames"]
|
||||
|
||||
assert {
|
||||
"@type": "asset",
|
||||
"value": {"assetId": "Alexa.Setting.Opening"},
|
||||
} in capability_resources["friendlyNames"]
|
||||
|
||||
configuration = range_capability["configuration"]
|
||||
assert configuration is not None
|
||||
assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent"
|
||||
|
||||
supported_range = configuration["supportedRange"]
|
||||
assert supported_range["minimumValue"] == 0
|
||||
assert supported_range["maximumValue"] == 100
|
||||
assert supported_range["precision"] == 1
|
||||
|
||||
# Assert for Position Semantics
|
||||
position_semantics = range_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in position_action_mappings
|
||||
|
||||
position_state_mappings = position_semantics["stateMappings"]
|
||||
assert position_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in position_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in position_state_mappings
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_range",
|
||||
service_call,
|
||||
hass,
|
||||
payload={"rangeValue": position},
|
||||
instance="cover.position",
|
||||
)
|
||||
assert call.data.get("position") == position_attr_in_service_call
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == position
|
||||
|
||||
|
||||
async def test_cover_position_range(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test cover discovery and position range using rangeController.
|
||||
|
||||
Also tests an invalid cover position being handled correctly.
|
||||
"""
|
||||
|
||||
device = (
|
||||
"cover.test_range",
|
||||
"open",
|
||||
@ -1969,59 +2160,6 @@ async def test_cover_position_range(hass: HomeAssistant) -> None:
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in position_state_mappings
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_range",
|
||||
"cover.set_cover_position",
|
||||
hass,
|
||||
payload={"rangeValue": 50},
|
||||
instance="cover.position",
|
||||
)
|
||||
assert call.data["position"] == 50
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_range",
|
||||
"cover.close_cover",
|
||||
hass,
|
||||
payload={"rangeValue": 0},
|
||||
instance="cover.position",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 0
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_range",
|
||||
"cover.open_cover",
|
||||
hass,
|
||||
payload={"rangeValue": 100},
|
||||
instance="cover.position",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 100
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"cover#test_range",
|
||||
"cover.open_cover",
|
||||
hass,
|
||||
payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False},
|
||||
instance="cover.position",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 100
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
@ -3435,8 +3573,159 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
|
||||
assert {"name": "humanPresenceDetectionState"} in properties["supported"]
|
||||
|
||||
|
||||
async def test_cover_tilt_position_range(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"tilt_position",
|
||||
"tilt_position_attr_in_service_call",
|
||||
"supported_features",
|
||||
"service_call",
|
||||
),
|
||||
[
|
||||
(
|
||||
30,
|
||||
30,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
0,
|
||||
None,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT,
|
||||
"cover.close_cover_tilt",
|
||||
),
|
||||
(
|
||||
99,
|
||||
99,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
None,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT,
|
||||
"cover.open_cover_tilt",
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
CoverEntityFeature.SET_TILT_POSITION,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
60,
|
||||
60,
|
||||
CoverEntityFeature.SET_TILT_POSITION,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
100,
|
||||
CoverEntityFeature.SET_TILT_POSITION,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
(
|
||||
100,
|
||||
100,
|
||||
CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT,
|
||||
"cover.set_cover_tilt_position",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"tilt_position_30_open_close",
|
||||
"tilt_position_0_open_close",
|
||||
"tilt_position_99_open_close",
|
||||
"tilt_position_100_open_close",
|
||||
"tilt_position_0_no_open_close",
|
||||
"tilt_position_60_no_open_close",
|
||||
"tilt_position_100_no_open_close",
|
||||
"tilt_position_0_no_close",
|
||||
"tilt_position_100_no_open",
|
||||
],
|
||||
)
|
||||
async def test_cover_tilt_position(
|
||||
hass: HomeAssistant,
|
||||
tilt_position: int,
|
||||
tilt_position_attr_in_service_call: int | None,
|
||||
supported_features: CoverEntityFeature,
|
||||
service_call: str,
|
||||
) -> None:
|
||||
"""Test cover discovery and tilt position using rangeController."""
|
||||
device = (
|
||||
"cover.test_tilt_range",
|
||||
"open",
|
||||
{
|
||||
"friendly_name": "Test cover tilt range",
|
||||
"device_class": "blind",
|
||||
"supported_features": supported_features,
|
||||
"tilt_position": tilt_position,
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "cover#test_tilt_range"
|
||||
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||
assert appliance["friendlyName"] == "Test cover tilt range"
|
||||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PowerController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "cover.tilt"
|
||||
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
|
||||
action_mappings = semantics["actionMappings"]
|
||||
assert action_mappings is not None
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_tilt_range",
|
||||
service_call,
|
||||
hass,
|
||||
payload={"rangeValue": tilt_position},
|
||||
instance="cover.tilt",
|
||||
)
|
||||
assert call.data.get("tilt_position") == tilt_position_attr_in_service_call
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == tilt_position
|
||||
|
||||
|
||||
async def test_cover_tilt_position_range(hass: HomeAssistant) -> None:
|
||||
"""Test cover discovery and tilt position range using rangeController.
|
||||
|
||||
Also tests and invalid tilt position being handled correctly.
|
||||
"""
|
||||
device = (
|
||||
"cover.test_tilt_range",
|
||||
"open",
|
||||
@ -3485,48 +3774,6 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert call.data["tilt_position"] == 50
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_tilt_range",
|
||||
"cover.close_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": 0},
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 0
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"cover#test_tilt_range",
|
||||
"cover.open_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": 100},
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 100
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"cover#test_tilt_range",
|
||||
"cover.open_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False},
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
assert properties["namespace"] == "Alexa.RangeController"
|
||||
assert properties["value"] == 100
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
|
@ -662,15 +662,33 @@
|
||||
# ---
|
||||
# name: test_pipeline_empty_tts_output.1
|
||||
dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'tts_input': '',
|
||||
'voice': 'james_earl_jones',
|
||||
'conversation_id': None,
|
||||
'device_id': None,
|
||||
'engine': 'homeassistant',
|
||||
'intent_input': 'never mind',
|
||||
'language': 'en',
|
||||
})
|
||||
# ---
|
||||
# name: test_pipeline_empty_tts_output.2
|
||||
dict({
|
||||
'tts_output': dict({
|
||||
'intent_output': dict({
|
||||
'conversation_id': None,
|
||||
'response': dict({
|
||||
'card': dict({
|
||||
}),
|
||||
'data': dict({
|
||||
'failed': list([
|
||||
]),
|
||||
'success': list([
|
||||
]),
|
||||
'targets': list([
|
||||
]),
|
||||
}),
|
||||
'language': 'en',
|
||||
'response_type': 'action_done',
|
||||
'speech': dict({
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
@ -2467,10 +2467,10 @@ async def test_pipeline_empty_tts_output(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "assist_pipeline/run",
|
||||
"start_stage": "tts",
|
||||
"start_stage": "intent",
|
||||
"end_stage": "tts",
|
||||
"input": {
|
||||
"text": "",
|
||||
"text": "never mind",
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -2486,16 +2486,15 @@ async def test_pipeline_empty_tts_output(
|
||||
assert msg["event"]["data"] == snapshot
|
||||
events.append(msg["event"])
|
||||
|
||||
# text-to-speech
|
||||
# intent
|
||||
msg = await client.receive_json()
|
||||
assert msg["event"]["type"] == "tts-start"
|
||||
assert msg["event"]["type"] == "intent-start"
|
||||
assert msg["event"]["data"] == snapshot
|
||||
events.append(msg["event"])
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["event"]["type"] == "tts-end"
|
||||
assert msg["event"]["type"] == "intent-end"
|
||||
assert msg["event"]["data"] == snapshot
|
||||
assert not msg["event"]["data"]["tts_output"]
|
||||
events.append(msg["event"])
|
||||
|
||||
# run end
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""The tests for the webdav todo component."""
|
||||
from datetime import UTC, date, datetime
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
@ -200,12 +201,16 @@ async def test_supported_components(
|
||||
),
|
||||
(
|
||||
{"due_date": "2023-11-18"},
|
||||
{"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118"},
|
||||
{"status": "NEEDS-ACTION", "summary": "Cheese", "due": date(2023, 11, 18)},
|
||||
{**RESULT_ITEM, "due": "2023-11-18"},
|
||||
),
|
||||
(
|
||||
{"due_datetime": "2023-11-18T08:30:00-06:00"},
|
||||
{"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118T143000Z"},
|
||||
{
|
||||
"status": "NEEDS-ACTION",
|
||||
"summary": "Cheese",
|
||||
"due": datetime(2023, 11, 18, 14, 30, 00, tzinfo=UTC),
|
||||
},
|
||||
{**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"},
|
||||
),
|
||||
(
|
||||
@ -311,13 +316,13 @@ async def test_add_item_failure(
|
||||
),
|
||||
(
|
||||
{"due_date": "2023-11-18"},
|
||||
["SUMMARY:Cheese", "DUE:20231118"],
|
||||
["SUMMARY:Cheese", "DUE;VALUE=DATE:20231118"],
|
||||
"1",
|
||||
{**RESULT_ITEM, "due": "2023-11-18"},
|
||||
),
|
||||
(
|
||||
{"due_datetime": "2023-11-18T08:30:00-06:00"},
|
||||
["SUMMARY:Cheese", "DUE:20231118T143000Z"],
|
||||
["SUMMARY:Cheese", "DUE;TZID=America/Regina:20231118T083000"],
|
||||
"1",
|
||||
{**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"},
|
||||
),
|
||||
|
@ -153,7 +153,7 @@ async def test_get_temperature(
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 10.0
|
||||
|
||||
# Select by area instead (climate_2)
|
||||
# Select by area (climate_2)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
@ -166,6 +166,19 @@ async def test_get_temperature(
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 22.0
|
||||
|
||||
# Select by name (climate_2)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
climate_intent.INTENT_GET_TEMPERATURE,
|
||||
{"name": {"value": "Climate 2"}},
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 22.0
|
||||
|
||||
|
||||
async def test_get_temperature_no_entities(
|
||||
hass: HomeAssistant,
|
||||
|
@ -107,18 +107,21 @@ async def test_token_refresh_success(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token_expiration_time", [12345])
|
||||
@pytest.mark.parametrize("closing", [True, False])
|
||||
async def test_token_requires_reauth(
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_credentials: None,
|
||||
closing: bool,
|
||||
) -> None:
|
||||
"""Test where token is expired and the refresh attempt requires reauth."""
|
||||
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
status=HTTPStatus.UNAUTHORIZED,
|
||||
closing=closing,
|
||||
)
|
||||
|
||||
assert not await integration_setup()
|
||||
|
@ -1301,6 +1301,7 @@ async def test_event_differs_timezone(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-11-30 12:15:00 +00:00")
|
||||
async def test_invalid_rrule_fix(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
|
@ -112,7 +112,8 @@
|
||||
"Bios Schema met Film Avond",
|
||||
"GF7 Woonkamer",
|
||||
"Badkamer Schema",
|
||||
"CV Jessie"
|
||||
"CV Jessie",
|
||||
"off"
|
||||
],
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-27T02:00:00+02:00",
|
||||
@ -251,7 +252,8 @@
|
||||
"Bios Schema met Film Avond",
|
||||
"GF7 Woonkamer",
|
||||
"Badkamer Schema",
|
||||
"CV Jessie"
|
||||
"CV Jessie",
|
||||
"off"
|
||||
],
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-08-02T02:00:00+02:00",
|
||||
@ -334,7 +336,8 @@
|
||||
"Bios Schema met Film Avond",
|
||||
"GF7 Woonkamer",
|
||||
"Badkamer Schema",
|
||||
"CV Jessie"
|
||||
"CV Jessie",
|
||||
"off"
|
||||
],
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-27T02:00:00+02:00",
|
||||
@ -344,7 +347,7 @@
|
||||
"model": "Lisa",
|
||||
"name": "Zone Lisa Bios",
|
||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||
"select_schedule": "None",
|
||||
"select_schedule": "off",
|
||||
"sensors": {
|
||||
"battery": 67,
|
||||
"setpoint": 13.0,
|
||||
@ -373,7 +376,8 @@
|
||||
"Bios Schema met Film Avond",
|
||||
"GF7 Woonkamer",
|
||||
"Badkamer Schema",
|
||||
"CV Jessie"
|
||||
"CV Jessie",
|
||||
"off"
|
||||
],
|
||||
"dev_class": "thermostatic_radiator_valve",
|
||||
"firmware": "2019-03-27T01:00:00+01:00",
|
||||
@ -383,7 +387,7 @@
|
||||
"model": "Tom/Floor",
|
||||
"name": "CV Kraan Garage",
|
||||
"preset_modes": ["home", "asleep", "away", "vacation", "no_frost"],
|
||||
"select_schedule": "None",
|
||||
"select_schedule": "off",
|
||||
"sensors": {
|
||||
"battery": 68,
|
||||
"setpoint": 5.5,
|
||||
@ -414,7 +418,8 @@
|
||||
"Bios Schema met Film Avond",
|
||||
"GF7 Woonkamer",
|
||||
"Badkamer Schema",
|
||||
"CV Jessie"
|
||||
"CV Jessie",
|
||||
"off"
|
||||
],
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-27T02:00:00+02:00",
|
||||
|
@ -59,7 +59,7 @@
|
||||
},
|
||||
"3cb70739631c4d17a86b8b12e8a5161b": {
|
||||
"active_preset": "home",
|
||||
"available_schedules": ["standaard"],
|
||||
"available_schedules": ["standaard", "off"],
|
||||
"dev_class": "thermostat",
|
||||
"firmware": "2018-02-08T11:15:53+01:00",
|
||||
"hardware": "6539-1301-5002",
|
||||
|
@ -52,7 +52,7 @@
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8": {
|
||||
"active_preset": "asleep",
|
||||
"available": true,
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test"],
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test", "off"],
|
||||
"control_state": "cooling",
|
||||
"dev_class": "thermostat",
|
||||
"location": "f2bf9048bef64cc5b6d5110154e33c81",
|
||||
@ -102,7 +102,7 @@
|
||||
"e2f4322d57924fa090fbbc48b3a140dc": {
|
||||
"active_preset": "home",
|
||||
"available": true,
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test"],
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test", "off"],
|
||||
"control_state": "off",
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-10T02:00:00+02:00",
|
||||
|
@ -80,7 +80,7 @@
|
||||
"ad4838d7d35c4d6ea796ee12ae5aedf8": {
|
||||
"active_preset": "asleep",
|
||||
"available": true,
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test"],
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test", "off"],
|
||||
"control_state": "preheating",
|
||||
"dev_class": "thermostat",
|
||||
"location": "f2bf9048bef64cc5b6d5110154e33c81",
|
||||
@ -124,7 +124,7 @@
|
||||
"e2f4322d57924fa090fbbc48b3a140dc": {
|
||||
"active_preset": "home",
|
||||
"available": true,
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test"],
|
||||
"available_schedules": ["Weekschema", "Badkamer", "Test", "off"],
|
||||
"control_state": "off",
|
||||
"dev_class": "zone_thermostat",
|
||||
"firmware": "2016-10-10T02:00:00+02:00",
|
||||
|
@ -59,7 +59,7 @@
|
||||
},
|
||||
"3cb70739631c4d17a86b8b12e8a5161b": {
|
||||
"active_preset": "home",
|
||||
"available_schedules": ["standaard"],
|
||||
"available_schedules": ["standaard", "off"],
|
||||
"dev_class": "thermostat",
|
||||
"firmware": "2018-02-08T11:15:53+01:00",
|
||||
"hardware": "6539-1301-5002",
|
||||
|
@ -59,7 +59,7 @@
|
||||
},
|
||||
"3cb70739631c4d17a86b8b12e8a5161b": {
|
||||
"active_preset": "home",
|
||||
"available_schedules": ["standaard"],
|
||||
"available_schedules": ["standaard", "off"],
|
||||
"dev_class": "thermostat",
|
||||
"firmware": "2018-02-08T11:15:53+01:00",
|
||||
"hardware": "6539-1301-5002",
|
||||
|
@ -115,6 +115,7 @@
|
||||
'GF7 Woonkamer',
|
||||
'Badkamer Schema',
|
||||
'CV Jessie',
|
||||
'off',
|
||||
]),
|
||||
'dev_class': 'zone_thermostat',
|
||||
'firmware': '2016-10-27T02:00:00+02:00',
|
||||
@ -260,6 +261,7 @@
|
||||
'GF7 Woonkamer',
|
||||
'Badkamer Schema',
|
||||
'CV Jessie',
|
||||
'off',
|
||||
]),
|
||||
'dev_class': 'zone_thermostat',
|
||||
'firmware': '2016-08-02T02:00:00+02:00',
|
||||
@ -349,6 +351,7 @@
|
||||
'GF7 Woonkamer',
|
||||
'Badkamer Schema',
|
||||
'CV Jessie',
|
||||
'off',
|
||||
]),
|
||||
'dev_class': 'zone_thermostat',
|
||||
'firmware': '2016-10-27T02:00:00+02:00',
|
||||
@ -364,7 +367,7 @@
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'select_schedule': 'None',
|
||||
'select_schedule': 'off',
|
||||
'sensors': dict({
|
||||
'battery': 67,
|
||||
'setpoint': 13.0,
|
||||
@ -394,6 +397,7 @@
|
||||
'GF7 Woonkamer',
|
||||
'Badkamer Schema',
|
||||
'CV Jessie',
|
||||
'off',
|
||||
]),
|
||||
'dev_class': 'thermostatic_radiator_valve',
|
||||
'firmware': '2019-03-27T01:00:00+01:00',
|
||||
@ -409,7 +413,7 @@
|
||||
'vacation',
|
||||
'no_frost',
|
||||
]),
|
||||
'select_schedule': 'None',
|
||||
'select_schedule': 'off',
|
||||
'sensors': dict({
|
||||
'battery': 68,
|
||||
'setpoint': 5.5,
|
||||
@ -441,6 +445,7 @@
|
||||
'GF7 Woonkamer',
|
||||
'Badkamer Schema',
|
||||
'CV Jessie',
|
||||
'off',
|
||||
]),
|
||||
'dev_class': 'zone_thermostat',
|
||||
'firmware': '2016-10-27T02:00:00+02:00',
|
||||
|
@ -31,6 +31,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -313,6 +315,21 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["swc"][0] = 1
|
||||
config["swn"][0] = "Test"
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass, mqtt_client_mock, mqtt_mock, Platform.BINARY_SENSOR, config
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -323,6 +340,18 @@ async def test_availability(
|
||||
await help_test_availability(hass, mqtt_mock, Platform.BINARY_SENSOR, config)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability when deep sleep is enabled."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["swc"][0] = 1
|
||||
config["swn"][0] = "Test"
|
||||
await help_test_deep_sleep_availability(
|
||||
hass, mqtt_mock, Platform.BINARY_SENSOR, config
|
||||
)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -4,6 +4,7 @@ import json
|
||||
from unittest.mock import ANY
|
||||
|
||||
from hatasmota.const import (
|
||||
CONF_DEEP_SLEEP,
|
||||
CONF_MAC,
|
||||
CONF_OFFLINE,
|
||||
CONF_ONLINE,
|
||||
@ -188,6 +189,76 @@ async def help_test_availability_when_connection_lost(
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass,
|
||||
mqtt_client_mock,
|
||||
mqtt_mock,
|
||||
domain,
|
||||
config,
|
||||
sensor_config=None,
|
||||
object_id="tasmota_test",
|
||||
):
|
||||
"""Test availability after MQTT disconnection when deep sleep is enabled.
|
||||
|
||||
This is a test helper for the TasmotaAvailability mixin.
|
||||
"""
|
||||
config[CONF_DEEP_SLEEP] = 1
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config",
|
||||
json.dumps(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
if sensor_config:
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
|
||||
json.dumps(sensor_config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device online
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Disconnected from MQTT server -> state changed to unavailable
|
||||
mqtt_mock.connected = False
|
||||
await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Reconnected to MQTT server -> state no longer unavailable
|
||||
mqtt_mock.connected = True
|
||||
await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Receive LWT again
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
get_topic_tele_will(config),
|
||||
config_get_state_online(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
get_topic_tele_will(config),
|
||||
config_get_state_offline(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def help_test_availability(
|
||||
hass,
|
||||
mqtt_mock,
|
||||
@ -236,6 +307,55 @@ async def help_test_availability(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def help_test_deep_sleep_availability(
|
||||
hass,
|
||||
mqtt_mock,
|
||||
domain,
|
||||
config,
|
||||
sensor_config=None,
|
||||
object_id="tasmota_test",
|
||||
):
|
||||
"""Test availability when deep sleep is enabled.
|
||||
|
||||
This is a test helper for the TasmotaAvailability mixin.
|
||||
"""
|
||||
config[CONF_DEEP_SLEEP] = 1
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config",
|
||||
json.dumps(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
if sensor_config:
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors",
|
||||
json.dumps(sensor_config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
get_topic_tele_will(config),
|
||||
config_get_state_online(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
get_topic_tele_will(config),
|
||||
config_get_state_offline(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{object_id}")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def help_test_availability_discovery_update(
|
||||
hass,
|
||||
mqtt_mock,
|
||||
|
@ -22,6 +22,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -663,6 +665,27 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["dn"] = "Test"
|
||||
config["rl"][0] = 3
|
||||
config["rl"][1] = 3
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass,
|
||||
mqtt_client_mock,
|
||||
mqtt_mock,
|
||||
Platform.COVER,
|
||||
config,
|
||||
object_id="test_cover_1",
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -676,6 +699,19 @@ async def test_availability(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["dn"] = "Test"
|
||||
config["rl"][0] = 3
|
||||
config["rl"][1] = 3
|
||||
await help_test_deep_sleep_availability(
|
||||
hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1"
|
||||
)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -22,6 +22,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -232,6 +234,20 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["if"] = 1
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota"
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -243,6 +259,17 @@ async def test_availability(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["if"] = 1
|
||||
await help_test_deep_sleep_availability(
|
||||
hass, mqtt_mock, Platform.FAN, config, object_id="tasmota"
|
||||
)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -22,6 +22,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -1669,6 +1671,21 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["rl"][0] = 2
|
||||
config["lt_st"] = 1 # 1 channel light (Dimmer)
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -1679,6 +1696,16 @@ async def test_availability(
|
||||
await help_test_availability(hass, mqtt_mock, Platform.LIGHT, config)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["rl"][0] = 2
|
||||
config["lt_st"] = 1 # 1 channel light (Dimmer)
|
||||
await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.LIGHT, config)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -28,6 +28,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -1222,6 +1224,26 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass,
|
||||
mqtt_client_mock,
|
||||
mqtt_mock,
|
||||
Platform.SENSOR,
|
||||
config,
|
||||
sensor_config,
|
||||
"tasmota_dht11_temperature",
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -1238,6 +1260,22 @@ async def test_availability(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG)
|
||||
await help_test_deep_sleep_availability(
|
||||
hass,
|
||||
mqtt_mock,
|
||||
Platform.SENSOR,
|
||||
config,
|
||||
sensor_config,
|
||||
"tasmota_dht11_temperature",
|
||||
)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -20,6 +20,8 @@ from .test_common import (
|
||||
help_test_availability_discovery_update,
|
||||
help_test_availability_poll_state,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_deep_sleep_availability,
|
||||
help_test_deep_sleep_availability_when_connection_lost,
|
||||
help_test_discovery_device_remove,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_update_unchanged,
|
||||
@ -158,6 +160,20 @@ async def test_availability_when_connection_lost(
|
||||
)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability_when_connection_lost(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_tasmota,
|
||||
) -> None:
|
||||
"""Test availability after MQTT disconnection."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["rl"][0] = 1
|
||||
await help_test_deep_sleep_availability_when_connection_lost(
|
||||
hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config
|
||||
)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
@ -167,6 +183,15 @@ async def test_availability(
|
||||
await help_test_availability(hass, mqtt_mock, Platform.SWITCH, config)
|
||||
|
||||
|
||||
async def test_deep_sleep_availability(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
"""Test availability."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
config["rl"][0] = 1
|
||||
await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.SWITCH, config)
|
||||
|
||||
|
||||
async def test_availability_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
|
||||
) -> None:
|
||||
|
@ -5,7 +5,7 @@ from homeassistant.components.assist_pipeline.select import OPTION_PREFERRED
|
||||
from homeassistant.components.wyoming import DOMAIN
|
||||
from homeassistant.components.wyoming.devices import SatelliteDevice
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
@ -34,11 +34,11 @@ async def test_device_registry_info(
|
||||
assert assist_in_progress_state is not None
|
||||
assert assist_in_progress_state.state == STATE_OFF
|
||||
|
||||
satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass)
|
||||
assert satellite_enabled_id
|
||||
satellite_enabled_state = hass.states.get(satellite_enabled_id)
|
||||
assert satellite_enabled_state is not None
|
||||
assert satellite_enabled_state.state == STATE_ON
|
||||
muted_id = satellite_device.get_muted_entity_id(hass)
|
||||
assert muted_id
|
||||
muted_state = hass.states.get(muted_id)
|
||||
assert muted_state is not None
|
||||
assert muted_state.state == STATE_OFF
|
||||
|
||||
pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass)
|
||||
assert pipeline_entity_id
|
||||
@ -59,9 +59,9 @@ async def test_remove_device_registry_entry(
|
||||
assert assist_in_progress_id
|
||||
assert hass.states.get(assist_in_progress_id) is not None
|
||||
|
||||
satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass)
|
||||
assert satellite_enabled_id
|
||||
assert hass.states.get(satellite_enabled_id) is not None
|
||||
muted_id = satellite_device.get_muted_entity_id(hass)
|
||||
assert muted_id
|
||||
assert hass.states.get(muted_id) is not None
|
||||
|
||||
pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass)
|
||||
assert pipeline_entity_id
|
||||
@ -74,5 +74,5 @@ async def test_remove_device_registry_entry(
|
||||
|
||||
# Everything should be gone
|
||||
assert hass.states.get(assist_in_progress_id) is None
|
||||
assert hass.states.get(satellite_enabled_id) is None
|
||||
assert hass.states.get(muted_id) is None
|
||||
assert hass.states.get(pipeline_entity_id) is None
|
||||
|
@ -196,7 +196,7 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None:
|
||||
await mock_client.detect_event.wait()
|
||||
|
||||
assert not device.is_active
|
||||
assert device.is_enabled
|
||||
assert not device.is_muted
|
||||
|
||||
# Wake word is detected
|
||||
event_callback(
|
||||
@ -312,35 +312,36 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_satellite_disabled(hass: HomeAssistant) -> None:
|
||||
"""Test callback for a satellite that has been disabled."""
|
||||
on_disabled_event = asyncio.Event()
|
||||
async def test_satellite_muted(hass: HomeAssistant) -> None:
|
||||
"""Test callback for a satellite that has been muted."""
|
||||
on_muted_event = asyncio.Event()
|
||||
|
||||
original_make_satellite = wyoming._make_satellite
|
||||
|
||||
def make_disabled_satellite(
|
||||
def make_muted_satellite(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService
|
||||
):
|
||||
satellite = original_make_satellite(hass, config_entry, service)
|
||||
satellite.device.is_enabled = False
|
||||
satellite.device.set_is_muted(True)
|
||||
|
||||
return satellite
|
||||
|
||||
async def on_disabled(self):
|
||||
on_disabled_event.set()
|
||||
async def on_muted(self):
|
||||
self.device.set_is_muted(False)
|
||||
on_muted_event.set()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=SATELLITE_INFO,
|
||||
), patch(
|
||||
"homeassistant.components.wyoming._make_satellite", make_disabled_satellite
|
||||
"homeassistant.components.wyoming._make_satellite", make_muted_satellite
|
||||
), patch(
|
||||
"homeassistant.components.wyoming.satellite.WyomingSatellite.on_disabled",
|
||||
on_disabled,
|
||||
"homeassistant.components.wyoming.satellite.WyomingSatellite.on_muted",
|
||||
on_muted,
|
||||
):
|
||||
await setup_config_entry(hass)
|
||||
async with asyncio.timeout(1):
|
||||
await on_disabled_event.wait()
|
||||
await on_muted_event.wait()
|
||||
|
||||
|
||||
async def test_satellite_restart(hass: HomeAssistant) -> None:
|
||||
@ -368,11 +369,19 @@ async def test_satellite_restart(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_satellite_reconnect(hass: HomeAssistant) -> None:
|
||||
"""Test satellite reconnect call after connection refused."""
|
||||
on_reconnect_event = asyncio.Event()
|
||||
num_reconnects = 0
|
||||
reconnect_event = asyncio.Event()
|
||||
stopped_event = asyncio.Event()
|
||||
|
||||
async def on_reconnect(self):
|
||||
self.stop()
|
||||
on_reconnect_event.set()
|
||||
nonlocal num_reconnects
|
||||
num_reconnects += 1
|
||||
if num_reconnects >= 2:
|
||||
reconnect_event.set()
|
||||
self.stop()
|
||||
|
||||
async def on_stopped(self):
|
||||
stopped_event.set()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
@ -383,10 +392,14 @@ async def test_satellite_reconnect(hass: HomeAssistant) -> None:
|
||||
), patch(
|
||||
"homeassistant.components.wyoming.satellite.WyomingSatellite.on_reconnect",
|
||||
on_reconnect,
|
||||
), patch(
|
||||
"homeassistant.components.wyoming.satellite.WyomingSatellite.on_stopped",
|
||||
on_stopped,
|
||||
):
|
||||
await setup_config_entry(hass)
|
||||
async with asyncio.timeout(1):
|
||||
await on_reconnect_event.wait()
|
||||
await reconnect_event.wait()
|
||||
await stopped_event.wait()
|
||||
|
||||
|
||||
async def test_satellite_disconnect_before_pipeline(hass: HomeAssistant) -> None:
|
||||
|
@ -5,28 +5,28 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_satellite_enabled(
|
||||
async def test_muted(
|
||||
hass: HomeAssistant,
|
||||
satellite_config_entry: ConfigEntry,
|
||||
satellite_device: SatelliteDevice,
|
||||
) -> None:
|
||||
"""Test satellite enabled."""
|
||||
satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass)
|
||||
assert satellite_enabled_id
|
||||
"""Test satellite muted."""
|
||||
muted_id = satellite_device.get_muted_entity_id(hass)
|
||||
assert muted_id
|
||||
|
||||
state = hass.states.get(satellite_enabled_id)
|
||||
state = hass.states.get(muted_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
assert satellite_device.is_enabled
|
||||
assert state.state == STATE_OFF
|
||||
assert not satellite_device.is_muted
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
{"entity_id": satellite_enabled_id},
|
||||
"turn_on",
|
||||
{"entity_id": muted_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(satellite_enabled_id)
|
||||
state = hass.states.get(muted_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
assert not satellite_device.is_enabled
|
||||
assert state.state == STATE_ON
|
||||
assert satellite_device.is_muted
|
||||
|
@ -46,7 +46,7 @@ def disable_request_retry_delay():
|
||||
with patch(
|
||||
"homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR",
|
||||
zigpy.util.retryable_request(tries=3, delay=0),
|
||||
), patch("homeassistant.components.zha.STARTUP_FAILURE_DELAY_S", 0.01):
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
"""Test ZHA Gateway."""
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from zigpy.application import ControllerApplication
|
||||
import zigpy.profiles.zha as zha
|
||||
import zigpy.zcl.clusters.general as general
|
||||
import zigpy.zcl.clusters.lighting as lighting
|
||||
@ -223,48 +222,6 @@ async def test_gateway_create_group_with_id(
|
||||
assert zha_group.group_id == 0x1234
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices",
|
||||
MagicMock(),
|
||||
)
|
||||
@patch(
|
||||
"homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups",
|
||||
MagicMock(),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("device_path", "thread_state", "config_override"),
|
||||
[
|
||||
("/dev/ttyUSB0", True, {}),
|
||||
("socket://192.168.1.123:9999", False, {}),
|
||||
("socket://192.168.1.123:9999", True, {"use_thread": True}),
|
||||
],
|
||||
)
|
||||
async def test_gateway_initialize_bellows_thread(
|
||||
device_path: str,
|
||||
thread_state: bool,
|
||||
config_override: dict,
|
||||
hass: HomeAssistant,
|
||||
zigpy_app_controller: ControllerApplication,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test ZHA disabling the UART thread when connecting to a TCP coordinator."""
|
||||
config_entry.data = dict(config_entry.data)
|
||||
config_entry.data["device"]["path"] = device_path
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry)
|
||||
|
||||
with patch(
|
||||
"bellows.zigbee.application.ControllerApplication.new",
|
||||
return_value=zigpy_app_controller,
|
||||
) as mock_new:
|
||||
await zha_gateway.async_initialize()
|
||||
|
||||
mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state
|
||||
|
||||
await zha_gateway.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_path", "config_override", "expected_channel"),
|
||||
[
|
||||
|
@ -95,7 +95,6 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None:
|
||||
assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER
|
||||
|
||||
|
||||
@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1)
|
||||
@pytest.mark.parametrize(
|
||||
("detected_hardware", "expected_learn_more_url"),
|
||||
[
|
||||
@ -176,7 +175,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
@ -189,7 +188,6 @@ async def test_multipan_firmware_no_repair_on_probe_failure(
|
||||
assert issue is None
|
||||
|
||||
|
||||
@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1)
|
||||
async def test_multipan_firmware_retry_on_probe_ezsp(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
Loading…
x
Reference in New Issue
Block a user