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