This commit is contained in:
Franck Nijhof 2023-12-13 18:48:43 +01:00 committed by GitHub
commit 711d9e21ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1218 additions and 540 deletions

View File

@ -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]},

View File

@ -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."

View File

@ -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}":

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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."
} }
} }
} }

View File

@ -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"]
} }

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
) )

View File

@ -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"]
} }

View File

@ -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

View File

@ -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"]
} }

View File

@ -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"]
} }

View File

@ -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."""

View File

@ -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"]
) )

View File

@ -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
) )

View File

@ -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
) )

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -108,7 +108,10 @@
} }
}, },
"select_schedule": { "select_schedule": {
"name": "Thermostat schedule" "name": "Thermostat schedule",
"state": {
"off": "Off"
}
} }
}, },
"sensor": { "sensor": {

View File

@ -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"]
} }

View File

@ -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

View File

@ -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"]
} }

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -42,8 +42,8 @@
} }
}, },
"switch": { "switch": {
"satellite_enabled": { "mute": {
"name": "Satellite enabled" "name": "Mute"
} }
} }
} }

View File

@ -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)

View File

@ -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"]
} }

View File

@ -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)

View File

@ -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"
) )

View File

@ -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 (

View File

@ -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",

View File

@ -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(

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "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)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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({
}),
}),
}), }),
}) })
# --- # ---

View File

@ -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

View File

@ -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"},
), ),

View File

@ -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,

View File

@ -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()

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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',

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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"),
[ [

View File

@ -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,