mirror of
https://github.com/home-assistant/core.git
synced 2025-11-25 02:28:03 +00:00
Compare commits
1 Commits
ble_provis
...
adguard/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05917a9fcd |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -391,8 +391,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dsmr/ @Robbie1221
|
||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/duckdns/ @tr4nt0r
|
||||
/tests/components/duckdns/ @tr4nt0r
|
||||
/homeassistant/components/duke_energy/ @hunterjm
|
||||
/tests/components/duke_energy/ @hunterjm
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
|
||||
@@ -71,26 +71,9 @@ class BangOlufsenModel(StrEnum):
|
||||
BEOSOUND_BALANCE = "Beosound Balance"
|
||||
BEOSOUND_EMERGE = "Beosound Emerge"
|
||||
BEOSOUND_LEVEL = "Beosound Level"
|
||||
BEOSOUND_PREMIERE = "Beosound Premiere"
|
||||
BEOSOUND_THEATRE = "Beosound Theatre"
|
||||
|
||||
|
||||
# Physical "buttons" on devices
|
||||
class BangOlufsenButtons(StrEnum):
|
||||
"""Enum for device buttons."""
|
||||
|
||||
BLUETOOTH = "Bluetooth"
|
||||
MICROPHONE = "Microphone"
|
||||
NEXT = "Next"
|
||||
PLAY_PAUSE = "PlayPause"
|
||||
PRESET_1 = "Preset1"
|
||||
PRESET_2 = "Preset2"
|
||||
PRESET_3 = "Preset3"
|
||||
PRESET_4 = "Preset4"
|
||||
PREVIOUS = "Previous"
|
||||
VOLUME = "Volume"
|
||||
|
||||
|
||||
# Dispatcher events
|
||||
class WebsocketNotification(StrEnum):
|
||||
"""Enum for WebSocket notification types."""
|
||||
@@ -221,6 +204,23 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
),
|
||||
]
|
||||
)
|
||||
# Map for storing compatibility of devices.
|
||||
|
||||
MODEL_SUPPORT_DEVICE_BUTTONS: Final[str] = "device_buttons"
|
||||
|
||||
MODEL_SUPPORT_MAP = {
|
||||
MODEL_SUPPORT_DEVICE_BUTTONS: (
|
||||
BangOlufsenModel.BEOLAB_8,
|
||||
BangOlufsenModel.BEOLAB_28,
|
||||
BangOlufsenModel.BEOSOUND_2,
|
||||
BangOlufsenModel.BEOSOUND_A5,
|
||||
BangOlufsenModel.BEOSOUND_A9,
|
||||
BangOlufsenModel.BEOSOUND_BALANCE,
|
||||
BangOlufsenModel.BEOSOUND_EMERGE,
|
||||
BangOlufsenModel.BEOSOUND_LEVEL,
|
||||
BangOlufsenModel.BEOSOUND_THEATRE,
|
||||
)
|
||||
}
|
||||
|
||||
# Device events
|
||||
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
|
||||
@@ -236,7 +236,18 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
|
||||
|
||||
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
||||
|
||||
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
|
||||
DEVICE_BUTTONS: Final[list[str]] = [
|
||||
"Bluetooth",
|
||||
"Microphone",
|
||||
"Next",
|
||||
"PlayPause",
|
||||
"Preset1",
|
||||
"Preset2",
|
||||
"Preset3",
|
||||
"Preset4",
|
||||
"Previous",
|
||||
"Volume",
|
||||
]
|
||||
|
||||
|
||||
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
|
||||
|
||||
@@ -6,13 +6,11 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .util import get_device_buttons
|
||||
from .const import DEVICE_BUTTONS, DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -42,7 +40,7 @@ async def async_get_config_entry_diagnostics(
|
||||
data["media_player"] = state_dict
|
||||
|
||||
# Add button Event entity states (if enabled)
|
||||
for device_button in get_device_buttons(config_entry.data[CONF_MODEL]):
|
||||
for device_button in DEVICE_BUTTONS:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
EVENT_DOMAIN, DOMAIN, f"{config_entry.unique_id}_{device_button}"
|
||||
):
|
||||
|
||||
@@ -9,9 +9,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import CONNECTION_STATUS, DEVICE_BUTTON_EVENTS, WebsocketNotification
|
||||
from .const import (
|
||||
CONNECTION_STATUS,
|
||||
DEVICE_BUTTON_EVENTS,
|
||||
DEVICE_BUTTONS,
|
||||
MODEL_SUPPORT_DEVICE_BUTTONS,
|
||||
MODEL_SUPPORT_MAP,
|
||||
WebsocketNotification,
|
||||
)
|
||||
from .entity import BangOlufsenEntity
|
||||
from .util import get_device_buttons
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -23,10 +29,11 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Sensor entities from config entry."""
|
||||
|
||||
async_add_entities(
|
||||
BangOlufsenButtonEvent(config_entry, button_type)
|
||||
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
||||
)
|
||||
if config_entry.data[CONF_MODEL] in MODEL_SUPPORT_MAP[MODEL_SUPPORT_DEVICE_BUTTONS]:
|
||||
async_add_entities(
|
||||
BangOlufsenButtonEvent(config_entry, button_type)
|
||||
for button_type in DEVICE_BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class BangOlufsenButtonEvent(BangOlufsenEntity, EventEntity):
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
||||
@@ -21,18 +21,3 @@ def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
||||
def get_serial_number_from_jid(jid: str) -> str:
|
||||
"""Get serial number from Beolink JID."""
|
||||
return jid.split(".")[2].split("@")[0]
|
||||
|
||||
|
||||
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
|
||||
"""Get supported buttons for a given model."""
|
||||
buttons = DEVICE_BUTTONS.copy()
|
||||
|
||||
# Beosound Premiere does not have a bluetooth button
|
||||
if model == BangOlufsenModel.BEOSOUND_PREMIERE:
|
||||
buttons.remove(BangOlufsenButtons.BLUETOOTH)
|
||||
|
||||
# Beoconnect Core does not have any buttons
|
||||
elif model == BangOlufsenModel.BEOCONNECT_CORE:
|
||||
buttons = []
|
||||
|
||||
return buttons
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Any, cast
|
||||
from aiohttp import ClientSession
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
@@ -19,17 +18,13 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_TXT = "txt"
|
||||
@@ -37,13 +32,7 @@ ATTR_TXT = "txt"
|
||||
DOMAIN = "duckdns"
|
||||
|
||||
INTERVAL = timedelta(minutes=5)
|
||||
BACKOFF_INTERVALS = (
|
||||
INTERVAL,
|
||||
timedelta(minutes=1),
|
||||
timedelta(minutes=5),
|
||||
timedelta(minutes=15),
|
||||
timedelta(minutes=30),
|
||||
)
|
||||
|
||||
SERVICE_SET_TXT = "set_txt"
|
||||
|
||||
UPDATE_URL = "https://www.duckdns.org/update"
|
||||
@@ -60,112 +49,39 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_TXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_CONFIG_ENTRY): ConfigEntrySelector(
|
||||
{
|
||||
"integration": DOMAIN,
|
||||
}
|
||||
),
|
||||
vol.Optional(ATTR_TXT): vol.Any(None, cv.string),
|
||||
}
|
||||
)
|
||||
|
||||
type DuckDnsConfigEntry = ConfigEntry
|
||||
SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)})
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Initialize the DuckDNS component."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TXT,
|
||||
update_domain_service,
|
||||
schema=SERVICE_TXT_SCHEMA,
|
||||
)
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
|
||||
"""Set up Duck DNS from a config entry."""
|
||||
|
||||
domain: str = config[DOMAIN][CONF_DOMAIN]
|
||||
token: str = config[DOMAIN][CONF_ACCESS_TOKEN]
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
async def update_domain_interval(_now: datetime) -> bool:
|
||||
"""Update the DuckDNS entry."""
|
||||
return await _update_duckdns(
|
||||
session,
|
||||
entry.data[CONF_DOMAIN],
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
)
|
||||
return await _update_duckdns(session, domain, token)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval_backoff(
|
||||
hass, update_domain_interval, BACKOFF_INTERVALS
|
||||
)
|
||||
intervals = (
|
||||
INTERVAL,
|
||||
timedelta(minutes=1),
|
||||
timedelta(minutes=5),
|
||||
timedelta(minutes=15),
|
||||
timedelta(minutes=30),
|
||||
)
|
||||
async_track_time_interval_backoff(hass, update_domain_interval, intervals)
|
||||
|
||||
async def update_domain_service(call: ServiceCall) -> None:
|
||||
"""Update the DuckDNS entry."""
|
||||
await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_config_entry(
|
||||
hass: HomeAssistant, entry_id: str | None = None
|
||||
) -> DuckDnsConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
|
||||
if entry_id is None:
|
||||
if not (config_entries := hass.config_entries.async_entries(DOMAIN)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
|
||||
if len(config_entries) != 1:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_selected",
|
||||
)
|
||||
return config_entries[0]
|
||||
|
||||
if not (entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def update_domain_service(call: ServiceCall) -> None:
|
||||
"""Update the DuckDNS entry."""
|
||||
|
||||
entry = get_config_entry(call.hass, call.data.get(ATTR_CONFIG_ENTRY))
|
||||
|
||||
session = async_get_clientsession(call.hass)
|
||||
|
||||
await _update_duckdns(
|
||||
session,
|
||||
entry.data[CONF_DOMAIN],
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
txt=call.data.get(ATTR_TXT),
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Config flow for the Duck DNS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from . import _update_duckdns
|
||||
from .const import DOMAIN
|
||||
from .issue import deprecate_yaml_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DOMAIN): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.TEXT, suffix=".duckdns.org")
|
||||
),
|
||||
vol.Required(CONF_ACCESS_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Duck DNS."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_DOMAIN: user_input[CONF_DOMAIN]})
|
||||
session = async_get_clientsession(self.hass)
|
||||
try:
|
||||
if not await _update_duckdns(
|
||||
session,
|
||||
user_input[CONF_DOMAIN],
|
||||
user_input[CONF_ACCESS_TOKEN],
|
||||
):
|
||||
errors["base"] = "update_failed"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_DOMAIN]}.duckdns.org", data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={"url": "https://www.duckdns.org/"},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import config from yaml."""
|
||||
|
||||
self._async_abort_entries_match({CONF_DOMAIN: import_info[CONF_DOMAIN]})
|
||||
result = await self.async_step_user(import_info)
|
||||
if errors := result.get("errors"):
|
||||
deprecate_yaml_issue(self.hass, import_success=False)
|
||||
return self.async_abort(reason=errors["base"])
|
||||
|
||||
deprecate_yaml_issue(self.hass, import_success=True)
|
||||
return result
|
||||
@@ -1,7 +0,0 @@
|
||||
"""Constants for the Duck DNS integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN = "duckdns"
|
||||
|
||||
ATTR_CONFIG_ENTRY: Final = "config_entry_id"
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Issues for Duck DNS integration."""
|
||||
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def deprecate_yaml_issue(hass: HomeAssistant, *, import_success: bool) -> None:
|
||||
"""Deprecate yaml issue."""
|
||||
if import_success:
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2026.6.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Duck DNS",
|
||||
},
|
||||
)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_error",
|
||||
breaks_in_ha_version="2026.6.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_error",
|
||||
translation_placeholders={
|
||||
"url": "/config/integrations/dashboard/add?domain=duckdns"
|
||||
},
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"domain": "duckdns",
|
||||
"name": "Duck DNS",
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duckdns",
|
||||
"iot_class": "cloud_polling"
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
set_txt:
|
||||
fields:
|
||||
config_entry_id:
|
||||
selector:
|
||||
config_entry:
|
||||
integration: duckdns
|
||||
txt:
|
||||
required: true
|
||||
example: "This domain name is reserved for use in documentation"
|
||||
selector:
|
||||
text:
|
||||
|
||||
@@ -1,48 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"update_failed": "Updating Duck DNS failed"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Token",
|
||||
"domain": "Subdomain"
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "Your Duck DNS account token",
|
||||
"domain": "The Duck DNS subdomain to update"
|
||||
},
|
||||
"description": "Enter your Duck DNS subdomain and token below to configure dynamic DNS updates. You can find your token on the [Duck DNS]({url}) homepage after logging into your account."
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"entry_not_found": {
|
||||
"message": "Duck DNS integration entry not found"
|
||||
},
|
||||
"entry_not_selected": {
|
||||
"message": "Duck DNS integration entry not selected"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_error": {
|
||||
"description": "Configuring Duck DNS using YAML is being removed but there was an error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the Duck DNS YAML configuration from your `configuration.yaml` file and continue to [set up the integration]({url}) manually.",
|
||||
"title": "The Duck DNS YAML configuration import failed"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_txt": {
|
||||
"description": "Sets the TXT record of your Duck DNS subdomain.",
|
||||
"description": "Sets the TXT record of your DuckDNS subdomain.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "The Duck DNS integration ID.",
|
||||
"name": "Integration ID"
|
||||
},
|
||||
"txt": {
|
||||
"description": "Payload for the TXT record.",
|
||||
"name": "TXT"
|
||||
|
||||
@@ -20,7 +20,7 @@ from .coordinator import (
|
||||
GoogleWeatherSubEntryRuntimeData,
|
||||
)
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WEATHER]
|
||||
_PLATFORMS: list[Platform] = [Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -16,15 +16,10 @@ class GoogleWeatherBaseEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
unique_id_suffix: str | None = None,
|
||||
self, config_entry: GoogleWeatherConfigEntry, subentry: ConfigSubentry
|
||||
) -> None:
|
||||
"""Initialize base entity."""
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
if unique_id_suffix is not None:
|
||||
self._attr_unique_id += f"_{unique_id_suffix.lower()}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"cloud_coverage": {
|
||||
"default": "mdi:weather-cloudy"
|
||||
},
|
||||
"precipitation_probability": {
|
||||
"default": "mdi:weather-rainy"
|
||||
},
|
||||
"precipitation_qpf": {
|
||||
"default": "mdi:cup-water"
|
||||
},
|
||||
"thunderstorm_probability": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"uv_index": {
|
||||
"default": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"visibility": {
|
||||
"default": "mdi:eye"
|
||||
},
|
||||
"weather_condition": {
|
||||
"default": "mdi:card-text-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
"""Support for Google Weather sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from google_weather_api import CurrentConditionsResponse
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import (
|
||||
GoogleWeatherConfigEntry,
|
||||
GoogleWeatherCurrentConditionsCoordinator,
|
||||
)
|
||||
from .entity import GoogleWeatherBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GoogleWeatherSensorDescription(SensorEntityDescription):
|
||||
"""Class describing Google Weather sensor entities."""
|
||||
|
||||
value_fn: Callable[[CurrentConditionsResponse], str | int | float | None]
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[GoogleWeatherSensorDescription, ...] = (
|
||||
GoogleWeatherSensorDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.temperature.degrees,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="feelsLikeTemperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.feels_like_temperature.degrees,
|
||||
translation_key="apparent_temperature",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="dewPoint",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.dew_point.degrees,
|
||||
translation_key="dew_point",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="heatIndex",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.heat_index.degrees,
|
||||
translation_key="heat_index",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="windChill",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.wind_chill.degrees,
|
||||
translation_key="wind_chill",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="relativeHumidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.relative_humidity,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="uvIndex",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: data.uv_index,
|
||||
translation_key="uv_index",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="precipitation_probability",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.precipitation.probability.percent,
|
||||
translation_key="precipitation_probability",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="precipitation_qpf",
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||
value_fn=lambda data: data.precipitation.qpf.quantity,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="thunderstormProbability",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.thunderstorm_probability,
|
||||
translation_key="thunderstorm_probability",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="airPressure",
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
value_fn=lambda data: data.air_pressure.mean_sea_level_millibars,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="wind_direction",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
native_unit_of_measurement=DEGREE,
|
||||
value_fn=lambda data: data.wind.direction.degrees,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="wind_speed",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: data.wind.speed.value,
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="wind_gust",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: data.wind.gust.value,
|
||||
translation_key="wind_gust_speed",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="visibility",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
value_fn=lambda data: data.visibility.distance,
|
||||
translation_key="visibility",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="cloudCover",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.cloud_cover,
|
||||
translation_key="cloud_coverage",
|
||||
),
|
||||
GoogleWeatherSensorDescription(
|
||||
key="weatherCondition",
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.weather_condition.description.text,
|
||||
translation_key="weather_condition",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GoogleWeatherConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Google Weather entities from a config_entry."""
|
||||
for subentry in entry.subentries.values():
|
||||
subentry_runtime_data = entry.runtime_data.subentries_runtime_data[
|
||||
subentry.subentry_id
|
||||
]
|
||||
coordinator = subentry_runtime_data.coordinator_observation
|
||||
async_add_entities(
|
||||
(
|
||||
GoogleWeatherSensor(coordinator, subentry, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.value_fn(coordinator.data) is not None
|
||||
),
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class GoogleWeatherSensor(
|
||||
CoordinatorEntity[GoogleWeatherCurrentConditionsCoordinator],
|
||||
GoogleWeatherBaseEntity,
|
||||
SensorEntity,
|
||||
):
|
||||
"""Define a Google Weather entity."""
|
||||
|
||||
entity_description: GoogleWeatherSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GoogleWeatherCurrentConditionsCoordinator,
|
||||
subentry: ConfigSubentry,
|
||||
description: GoogleWeatherSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
GoogleWeatherBaseEntity.__init__(
|
||||
self, coordinator.config_entry, subentry, description.key
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float | None:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
@@ -61,42 +61,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"apparent_temperature": {
|
||||
"name": "Apparent temperature"
|
||||
},
|
||||
"cloud_coverage": {
|
||||
"name": "Cloud coverage"
|
||||
},
|
||||
"dew_point": {
|
||||
"name": "Dew point"
|
||||
},
|
||||
"heat_index": {
|
||||
"name": "Heat index temperature"
|
||||
},
|
||||
"precipitation_probability": {
|
||||
"name": "Precipitation probability"
|
||||
},
|
||||
"thunderstorm_probability": {
|
||||
"name": "Thunderstorm probability"
|
||||
},
|
||||
"uv_index": {
|
||||
"name": "UV index"
|
||||
},
|
||||
"visibility": {
|
||||
"name": "Visibility"
|
||||
},
|
||||
"weather_condition": {
|
||||
"name": "Weather condition"
|
||||
},
|
||||
"wind_chill": {
|
||||
"name": "Wind chill temperature"
|
||||
},
|
||||
"wind_gust_speed": {
|
||||
"name": "Wind gust speed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@ ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
|
||||
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
|
||||
|
||||
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
|
||||
|
||||
CORE_CONTAINER = "homeassistant"
|
||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_STARTUP,
|
||||
@@ -50,7 +49,6 @@ from .const import (
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
||||
ISSUE_MOUNT_MOUNT_FAILED,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_ADDON_URL,
|
||||
PLACEHOLDER_KEY_FREE_SPACE,
|
||||
@@ -59,7 +57,7 @@ from .const import (
|
||||
STARTUP_COMPLETE,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator, get_addons_info, get_host_info
|
||||
from .coordinator import get_addons_info, get_host_info
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
@@ -79,7 +77,7 @@ UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
# Keys (type + context) of issues that when found should be made into a repair
|
||||
ISSUE_KEYS_FOR_REPAIRS = {
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_MOUNT_MOUNT_FAILED,
|
||||
"issue_mount_mount_failed",
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
@@ -286,9 +284,6 @@ class SupervisorIssues:
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2"
|
||||
|
||||
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
|
||||
self._async_coordinator_refresh()
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
@@ -341,9 +336,6 @@ class SupervisorIssues:
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
|
||||
|
||||
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
|
||||
self._async_coordinator_refresh()
|
||||
|
||||
del self._issues[issue.uuid]
|
||||
|
||||
def get_issue(self, issue_id: str) -> Issue | None:
|
||||
@@ -414,11 +406,3 @@ class SupervisorIssues:
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
|
||||
self.remove_issue(Issue.from_dict(event[ATTR_DATA]))
|
||||
|
||||
def _async_coordinator_refresh(self) -> None:
|
||||
"""Refresh coordinator to update latest data in entities."""
|
||||
coordinator: HassioDataUpdateCoordinator | None
|
||||
if coordinator := self._hass.data.get(ADDONS_COORDINATOR):
|
||||
coordinator.config_entry.async_create_task(
|
||||
self._hass, coordinator.async_refresh()
|
||||
)
|
||||
|
||||
@@ -13,13 +13,11 @@ DOMAIN = "home_connect"
|
||||
API_DEFAULT_RETRY_AFTER = 60
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"AirConditioner",
|
||||
"CleaningRobot",
|
||||
"CoffeeMaker",
|
||||
"Dishwasher",
|
||||
"Dryer",
|
||||
"Hood",
|
||||
"Microwave",
|
||||
"Oven",
|
||||
"WarmingDrawer",
|
||||
"Washer",
|
||||
@@ -85,14 +83,6 @@ PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
|
||||
FAN_SPEED_MODE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Automatic",
|
||||
"HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Manual",
|
||||
)
|
||||
}
|
||||
|
||||
AVAILABLE_MAPS_ENUM = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
@@ -325,10 +315,6 @@ PROGRAM_ENUM_OPTIONS = {
|
||||
options,
|
||||
)
|
||||
for option_key, options in (
|
||||
(
|
||||
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN_SPEED_MODE,
|
||||
FAN_SPEED_MODE_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
|
||||
@@ -82,12 +82,6 @@ set_program_and_options:
|
||||
- dishcare_dishwasher_program_maximum_cleaning
|
||||
- dishcare_dishwasher_program_mixed_load
|
||||
- dishcare_dishwasher_program_learning_dishwasher
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_active_clean
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_auto
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_cool
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_dry
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_fan
|
||||
- heating_ventilation_air_conditioning_air_conditioner_program_heat
|
||||
- laundry_care_dryer_program_cotton
|
||||
- laundry_care_dryer_program_synthetic
|
||||
- laundry_care_dryer_program_mix
|
||||
@@ -142,7 +136,6 @@ set_program_and_options:
|
||||
- cooking_oven_program_microwave_90_watt
|
||||
- cooking_oven_program_microwave_180_watt
|
||||
- cooking_oven_program_microwave_360_watt
|
||||
- cooking_oven_program_microwave_450_watt
|
||||
- cooking_oven_program_microwave_600_watt
|
||||
- cooking_oven_program_microwave_900_watt
|
||||
- cooking_oven_program_microwave_1000_watt
|
||||
@@ -184,28 +177,6 @@ set_program_and_options:
|
||||
- laundry_care_washer_dryer_program_easy_care
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_60
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_90
|
||||
air_conditioner_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_percentage:
|
||||
example: 50
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 100
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_mode:
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: fan_speed_mode
|
||||
options:
|
||||
- heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic
|
||||
- heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_manual
|
||||
cleaning_robot_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
|
||||
@@ -252,7 +252,6 @@
|
||||
"cooking_oven_program_microwave_1000_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_1000_watt%]",
|
||||
"cooking_oven_program_microwave_180_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_180_watt%]",
|
||||
"cooking_oven_program_microwave_360_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_360_watt%]",
|
||||
"cooking_oven_program_microwave_450_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_450_watt%]",
|
||||
"cooking_oven_program_microwave_600_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_600_watt%]",
|
||||
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
|
||||
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
|
||||
@@ -282,12 +281,6 @@
|
||||
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_65%]",
|
||||
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_steam_fresh%]",
|
||||
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_super_60%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_active_clean%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_auto": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_auto%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_cool": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_cool%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_dry": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_dry%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_fan%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_heat%]",
|
||||
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_anti_shrink%]",
|
||||
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_blankets%]",
|
||||
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts%]",
|
||||
@@ -450,13 +443,6 @@
|
||||
"laundry_care_dryer_enum_type_drying_target_iron_dry": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_iron_dry%]"
|
||||
}
|
||||
},
|
||||
"fan_speed_mode": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_mode::name%]",
|
||||
"state": {
|
||||
"heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic": "[%key:component::home_connect::selector::fan_speed_mode::options::heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_manual": "[%key:component::home_connect::selector::fan_speed_mode::options::heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_manual%]"
|
||||
}
|
||||
},
|
||||
"flow_rate": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_flow_rate::name%]",
|
||||
"state": {
|
||||
@@ -589,7 +575,6 @@
|
||||
"cooking_oven_program_microwave_1000_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_1000_watt%]",
|
||||
"cooking_oven_program_microwave_180_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_180_watt%]",
|
||||
"cooking_oven_program_microwave_360_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_360_watt%]",
|
||||
"cooking_oven_program_microwave_450_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_450_watt%]",
|
||||
"cooking_oven_program_microwave_600_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_600_watt%]",
|
||||
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
|
||||
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
|
||||
@@ -619,12 +604,6 @@
|
||||
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_65%]",
|
||||
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_steam_fresh%]",
|
||||
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_super_60%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_active_clean%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_auto": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_auto%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_cool": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_cool%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_dry": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_dry%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_fan%]",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_heat%]",
|
||||
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_anti_shrink%]",
|
||||
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_blankets%]",
|
||||
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts%]",
|
||||
@@ -1439,12 +1418,6 @@
|
||||
"laundry_care_dryer_enum_type_drying_target_iron_dry": "Iron dry"
|
||||
}
|
||||
},
|
||||
"fan_speed_mode": {
|
||||
"options": {
|
||||
"heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic": "Auto",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_manual": "Manual"
|
||||
}
|
||||
},
|
||||
"flow_rate": {
|
||||
"options": {
|
||||
"consumer_products_coffee_maker_enum_type_flow_rate_intense": "Intense",
|
||||
@@ -1553,7 +1526,6 @@
|
||||
"cooking_oven_program_microwave_1000_watt": "1000 Watt",
|
||||
"cooking_oven_program_microwave_180_watt": "180 Watt",
|
||||
"cooking_oven_program_microwave_360_watt": "360 Watt",
|
||||
"cooking_oven_program_microwave_450_watt": "450 Watt",
|
||||
"cooking_oven_program_microwave_600_watt": "600 Watt",
|
||||
"cooking_oven_program_microwave_900_watt": "900 Watt",
|
||||
"cooking_oven_program_microwave_90_watt": "90 Watt",
|
||||
@@ -1583,12 +1555,6 @@
|
||||
"dishcare_dishwasher_program_quick_65": "Quick 65ºC",
|
||||
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
||||
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "Active clean",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_auto": "Auto",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_cool": "Cool",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_dry": "Dry",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "Fan",
|
||||
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "Heat",
|
||||
"laundry_care_dryer_program_anti_shrink": "Anti shrink",
|
||||
"laundry_care_dryer_program_blankets": "Blankets",
|
||||
"laundry_care_dryer_program_business_shirts": "Business shirts",
|
||||
@@ -1857,14 +1823,6 @@
|
||||
"description": "Defines if the program sequence is optimized with special drying cycle ensures improved drying for glasses, plates and plasticware.",
|
||||
"name": "Zeolite dry"
|
||||
},
|
||||
"heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_mode": {
|
||||
"description": "Setting to adjust the fan speed mode to Manual or Auto.",
|
||||
"name": "Fan speed mode"
|
||||
},
|
||||
"heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_percentage": {
|
||||
"description": "Setting to adjust the venting level of the air conditioner as a percentage.",
|
||||
"name": "Fan speed percentage"
|
||||
},
|
||||
"laundry_care_dryer_option_drying_target": {
|
||||
"description": "Describes the drying target for a dryer program.",
|
||||
"name": "Drying target"
|
||||
@@ -1896,10 +1854,6 @@
|
||||
},
|
||||
"name": "Set program and options",
|
||||
"sections": {
|
||||
"air_conditioner_options": {
|
||||
"description": "Specific settings for air conditioners.",
|
||||
"name": "Air conditioner options"
|
||||
},
|
||||
"cleaning_robot_options": {
|
||||
"description": "Options for cleaning robots.",
|
||||
"name": "Cleaning robot options"
|
||||
|
||||
@@ -62,10 +62,6 @@
|
||||
},
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::description%]",
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
@@ -208,10 +204,6 @@
|
||||
"reconfigure_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::description%]",
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::title%]"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]"
|
||||
},
|
||||
|
||||
@@ -35,5 +35,3 @@ ZIGBEE_FLASHER_ADDON_SLUG = "core_silabs_flasher"
|
||||
|
||||
SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol"
|
||||
SILABS_FLASHER_ADDON_SLUG = "core_silabs_flasher"
|
||||
|
||||
Z2M_EMBER_DOCS_URL = "https://www.zigbee2mqtt.io/guide/adapters/emberznet.html"
|
||||
|
||||
@@ -33,7 +33,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import OTBR_DOMAIN, Z2M_EMBER_DOCS_URL, ZHA_DOMAIN
|
||||
from .const import OTBR_DOMAIN, ZHA_DOMAIN
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -456,7 +456,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
assert self._hardware_name is not None
|
||||
|
||||
if self._zigbee_integration == ZigbeeIntegration.OTHER:
|
||||
return await self.async_step_show_z2m_docs_url()
|
||||
return self._async_flow_finished()
|
||||
|
||||
result = await self.hass.config_entries.flow.async_init(
|
||||
ZHA_DOMAIN,
|
||||
@@ -475,21 +475,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
)
|
||||
return self._continue_zha_flow(result)
|
||||
|
||||
async def async_step_show_z2m_docs_url(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show Zigbee2MQTT documentation link."""
|
||||
if user_input is not None:
|
||||
return self._async_flow_finished()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="show_z2m_docs_url",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"z2m_docs_url": Z2M_EMBER_DOCS_URL,
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _continue_zha_flow(self, zha_result: ConfigFlowResult) -> ConfigFlowResult:
|
||||
"""Continue the ZHA flow."""
|
||||
|
||||
@@ -53,10 +53,6 @@
|
||||
},
|
||||
"title": "Pick your protocol"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "Your {model} is now running the latest Zigbee firmware.\nPlease read the Zigbee2MQTT documentation for EmberZNet adapters and copy the config for your {model}: {z2m_docs_url}",
|
||||
"title": "Set up Zigbee2MQTT"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "Configuring Thread"
|
||||
},
|
||||
|
||||
@@ -62,10 +62,6 @@
|
||||
},
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::description%]",
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
@@ -208,10 +204,6 @@
|
||||
"reconfigure_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::description%]",
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::title%]"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]"
|
||||
},
|
||||
|
||||
@@ -138,10 +138,6 @@
|
||||
"reconfigure_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]"
|
||||
},
|
||||
"show_z2m_docs_url": {
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::description%]",
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::show_z2m_docs_url::title%]"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
|
||||
from homematicip.base.functionalChannels import MultiModeInputChannel
|
||||
from homematicip.device import (
|
||||
AccelerationSensor,
|
||||
ContactInterface,
|
||||
@@ -88,11 +87,8 @@ async def async_setup_entry(
|
||||
entities.append(HomematicipTiltVibrationSensor(hap, device))
|
||||
if isinstance(device, WiredInput32):
|
||||
entities.extend(
|
||||
HomematicipMultiContactInterface(
|
||||
hap, device, channel_real_index=channel.index
|
||||
)
|
||||
for channel in device.functionalChannels
|
||||
if isinstance(channel, MultiModeInputChannel)
|
||||
HomematicipMultiContactInterface(hap, device, channel=channel)
|
||||
for channel in range(1, 33)
|
||||
)
|
||||
elif isinstance(device, FullFlushContactInterface6):
|
||||
entities.extend(
|
||||
@@ -231,24 +227,21 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt
|
||||
device,
|
||||
channel=1,
|
||||
is_multi_channel=True,
|
||||
channel_real_index=None,
|
||||
) -> None:
|
||||
"""Initialize the multi contact entity."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
channel_real_index=channel_real_index,
|
||||
hap, device, channel=channel, is_multi_channel=is_multi_channel
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the contact interface is on/open."""
|
||||
channel = self.get_channel_or_raise()
|
||||
if channel.windowState is None:
|
||||
if self._device.functionalChannels[self._channel].windowState is None:
|
||||
return None
|
||||
return channel.windowState != WindowState.CLOSED
|
||||
return (
|
||||
self._device.functionalChannels[self._channel].windowState
|
||||
!= WindowState.CLOSED
|
||||
)
|
||||
|
||||
|
||||
class HomematicipContactInterface(HomematicipMultiContactInterface, BinarySensorEntity):
|
||||
|
||||
@@ -283,23 +283,19 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Return if the cover is closed."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return channel.doorState == DoorState.CLOSED
|
||||
return self.functional_channel.doorState == DoorState.CLOSED
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.async_send_door_command(DoorCommand.OPEN)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.OPEN)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.async_send_door_command(DoorCommand.CLOSE)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.CLOSE)
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.async_send_door_command(DoorCommand.STOP)
|
||||
await self.functional_channel.async_send_door_command(DoorCommand.STOP)
|
||||
|
||||
|
||||
class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -85,7 +84,6 @@ class HomematicipGenericEntity(Entity):
|
||||
post: str | None = None,
|
||||
channel: int | None = None,
|
||||
is_multi_channel: bool | None = False,
|
||||
channel_real_index: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the generic entity."""
|
||||
self._hap = hap
|
||||
@@ -93,19 +91,8 @@ class HomematicipGenericEntity(Entity):
|
||||
self._device = device
|
||||
self._post = post
|
||||
self._channel = channel
|
||||
|
||||
# channel_real_index represents the actual index of the devices channel.
|
||||
# Accessing a functionalChannel by the channel parameter or array index is unreliable,
|
||||
# because the functionalChannels array is sorted as strings, not numbers.
|
||||
# For example, channels are ordered as: 1, 10, 11, 12, 2, 3, ...
|
||||
# Using channel_real_index ensures you reference the correct channel.
|
||||
self._channel_real_index: int | None = channel_real_index
|
||||
|
||||
self._is_multi_channel = is_multi_channel
|
||||
self.functional_channel = None
|
||||
with contextlib.suppress(ValueError):
|
||||
self.functional_channel = self.get_current_channel()
|
||||
|
||||
self.functional_channel = self.get_current_channel()
|
||||
# Marker showing that the HmIP device hase been removed.
|
||||
self.hmip_device_removed = False
|
||||
|
||||
@@ -114,20 +101,17 @@ class HomematicipGenericEntity(Entity):
|
||||
"""Return device specific attributes."""
|
||||
# Only physical devices should be HA devices.
|
||||
if isinstance(self._device, Device):
|
||||
device_id = str(self._device.id)
|
||||
home_id = str(self._device.homeId)
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers of Homematic IP device
|
||||
(DOMAIN, device_id)
|
||||
(DOMAIN, self._device.id)
|
||||
},
|
||||
manufacturer=self._device.oem,
|
||||
model=self._device.modelType,
|
||||
name=self._device.label,
|
||||
sw_version=self._device.firmwareVersion,
|
||||
# Link to the homematic ip access point.
|
||||
via_device=(DOMAIN, home_id),
|
||||
via_device=(DOMAIN, self._device.homeId),
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -201,31 +185,25 @@ class HomematicipGenericEntity(Entity):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the generic entity."""
|
||||
|
||||
name = ""
|
||||
name = None
|
||||
# Try to get a label from a channel.
|
||||
functional_channels = getattr(self._device, "functionalChannels", None)
|
||||
if functional_channels and self.functional_channel:
|
||||
if hasattr(self._device, "functionalChannels"):
|
||||
if self._is_multi_channel:
|
||||
label = getattr(self.functional_channel, "label", None)
|
||||
if label:
|
||||
name = str(label)
|
||||
elif len(functional_channels) > 1:
|
||||
label = getattr(functional_channels[1], "label", None)
|
||||
if label:
|
||||
name = str(label)
|
||||
name = self._device.functionalChannels[self._channel].label
|
||||
elif len(self._device.functionalChannels) > 1:
|
||||
name = self._device.functionalChannels[1].label
|
||||
|
||||
# Use device label, if name is not defined by channel label.
|
||||
if not name:
|
||||
name = self._device.label or ""
|
||||
name = self._device.label
|
||||
if self._post:
|
||||
name = f"{name} {self._post}"
|
||||
elif self._is_multi_channel:
|
||||
name = f"{name} Channel{self.get_channel_index()}"
|
||||
name = f"{name} Channel{self._channel}"
|
||||
|
||||
# Add a prefix to the name if the homematic ip home has a name.
|
||||
home_name = getattr(self._home, "name", None)
|
||||
if name and home_name:
|
||||
name = f"{home_name} {name}"
|
||||
if name and self._home.name:
|
||||
name = f"{self._home.name} {name}"
|
||||
|
||||
return name
|
||||
|
||||
@@ -239,7 +217,9 @@ class HomematicipGenericEntity(Entity):
|
||||
"""Return a unique ID."""
|
||||
unique_id = f"{self.__class__.__name__}_{self._device.id}"
|
||||
if self._is_multi_channel:
|
||||
unique_id = f"{self.__class__.__name__}_Channel{self.get_channel_index()}_{self._device.id}"
|
||||
unique_id = (
|
||||
f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
|
||||
)
|
||||
|
||||
return unique_id
|
||||
|
||||
@@ -274,65 +254,12 @@ class HomematicipGenericEntity(Entity):
|
||||
return state_attr
|
||||
|
||||
def get_current_channel(self) -> FunctionalChannel:
|
||||
"""Return the FunctionalChannel for the device.
|
||||
"""Return the FunctionalChannel for device."""
|
||||
if hasattr(self._device, "functionalChannels"):
|
||||
if self._is_multi_channel:
|
||||
return self._device.functionalChannels[self._channel]
|
||||
|
||||
Resolution priority:
|
||||
1. For multi-channel entities with a real index, find channel by index match.
|
||||
2. For multi-channel entities without a real index, use the provided channel position.
|
||||
3. For non multi-channel entities with >1 channels, use channel at position 1
|
||||
(index 0 is often a meta/service channel in HmIP).
|
||||
Raises ValueError if no suitable channel can be resolved.
|
||||
"""
|
||||
functional_channels = getattr(self._device, "functionalChannels", None)
|
||||
if not functional_channels:
|
||||
raise ValueError(
|
||||
f"Device {getattr(self._device, 'id', 'unknown')} has no functionalChannels"
|
||||
)
|
||||
if len(self._device.functionalChannels) > 1:
|
||||
return self._device.functionalChannels[1]
|
||||
|
||||
# Multi-channel handling
|
||||
if self._is_multi_channel:
|
||||
# Prefer real index mapping when provided to avoid ordering issues.
|
||||
if self._channel_real_index is not None:
|
||||
for channel in functional_channels:
|
||||
if channel.index == self._channel_real_index:
|
||||
return channel
|
||||
raise ValueError(
|
||||
f"Real channel index {self._channel_real_index} not found for device "
|
||||
f"{getattr(self._device, 'id', 'unknown')}"
|
||||
)
|
||||
# Fallback: positional channel (already sorted as strings upstream).
|
||||
if self._channel is not None and 0 <= self._channel < len(
|
||||
functional_channels
|
||||
):
|
||||
return functional_channels[self._channel]
|
||||
raise ValueError(
|
||||
f"Channel position {self._channel} invalid for device "
|
||||
f"{getattr(self._device, 'id', 'unknown')} (len={len(functional_channels)})"
|
||||
)
|
||||
|
||||
# Single-channel / non multi-channel entity: choose second element if available
|
||||
if len(functional_channels) > 1:
|
||||
return functional_channels[1]
|
||||
return functional_channels[0]
|
||||
|
||||
def get_channel_index(self) -> int:
|
||||
"""Return the correct channel index for this entity.
|
||||
|
||||
Prefers channel_real_index if set, otherwise returns channel.
|
||||
This ensures the correct channel is used even if the functionalChannels list is not numerically ordered.
|
||||
"""
|
||||
if self._channel_real_index is not None:
|
||||
return self._channel_real_index
|
||||
|
||||
if self._channel is not None:
|
||||
return self._channel
|
||||
|
||||
return 1
|
||||
|
||||
def get_channel_or_raise(self) -> FunctionalChannel:
|
||||
"""Return the FunctionalChannel or raise an error if not found."""
|
||||
if not self.functional_channel:
|
||||
raise ValueError(
|
||||
f"No functional channel found for device {getattr(self._device, 'id', 'unknown')}"
|
||||
)
|
||||
return self.functional_channel
|
||||
return None
|
||||
|
||||
@@ -92,9 +92,7 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
channel = self.get_channel_or_raise()
|
||||
channel.add_on_channel_event_handler(self._async_handle_event)
|
||||
self.functional_channel.add_on_channel_event_handler(self._async_handle_event)
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self, *args, **kwargs) -> None:
|
||||
|
||||
@@ -134,49 +134,49 @@ class HomematicipLightHS(HomematicipGenericEntity, LightEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return channel.on
|
||||
return self.functional_channel.on
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the current brightness."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return int(channel.dimLevel * 255.0)
|
||||
return int(self.functional_channel.dimLevel * 255.0)
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""Return the hue and saturation color value [float, float]."""
|
||||
channel = self.get_channel_or_raise()
|
||||
if channel.hue is None or channel.saturationLevel is None:
|
||||
if (
|
||||
self.functional_channel.hue is None
|
||||
or self.functional_channel.saturationLevel is None
|
||||
):
|
||||
return None
|
||||
return (
|
||||
channel.hue,
|
||||
channel.saturationLevel * 100.0,
|
||||
self.functional_channel.hue,
|
||||
self.functional_channel.saturationLevel * 100.0,
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
channel = self.get_channel_or_raise()
|
||||
|
||||
hs_color = kwargs.get(ATTR_HS_COLOR, (0.0, 0.0))
|
||||
hue = hs_color[0] % 360.0
|
||||
saturation = hs_color[1] / 100.0
|
||||
dim_level = round(kwargs.get(ATTR_BRIGHTNESS, 255) / 255.0, 2)
|
||||
|
||||
if ATTR_HS_COLOR not in kwargs:
|
||||
hue = channel.hue
|
||||
saturation = channel.saturationLevel
|
||||
hue = self.functional_channel.hue
|
||||
saturation = self.functional_channel.saturationLevel
|
||||
|
||||
if ATTR_BRIGHTNESS not in kwargs:
|
||||
# If no brightness is set, use the current brightness
|
||||
dim_level = channel.dimLevel or 1.0
|
||||
await channel.set_hue_saturation_dim_level_async(
|
||||
dim_level = self.functional_channel.dimLevel or 1.0
|
||||
|
||||
await self.functional_channel.set_hue_saturation_dim_level_async(
|
||||
hue=hue, saturation_level=saturation, dim_level=dim_level
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.set_switch_state_async(on=False)
|
||||
await self.functional_channel.set_switch_state_async(on=False)
|
||||
|
||||
|
||||
class HomematicipLightMeasuring(HomematicipLight):
|
||||
|
||||
@@ -307,8 +307,7 @@ class HomematicipWaterFlowSensor(HomematicipGenericEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return channel.waterFlow
|
||||
return self.functional_channel.waterFlow
|
||||
|
||||
|
||||
class HomematicipWaterVolumeSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
@@ -113,18 +113,15 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if switch is on."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return channel.on
|
||||
return self.functional_channel.on
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.async_turn_on()
|
||||
await self.functional_channel.async_turn_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.async_turn_off()
|
||||
await self.functional_channel.async_turn_off()
|
||||
|
||||
|
||||
class HomematicipSwitch(HomematicipMultiSwitch, SwitchEntity):
|
||||
|
||||
@@ -47,16 +47,13 @@ class HomematicipWateringValve(HomematicipGenericEntity, ValveEntity):
|
||||
|
||||
async def async_open_valve(self) -> None:
|
||||
"""Open the valve."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.set_watering_switch_state_async(True)
|
||||
await self.functional_channel.set_watering_switch_state_async(True)
|
||||
|
||||
async def async_close_valve(self) -> None:
|
||||
"""Close valve."""
|
||||
channel = self.get_channel_or_raise()
|
||||
await channel.set_watering_switch_state_async(False)
|
||||
await self.functional_channel.set_watering_switch_state_async(False)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Return if the valve is closed."""
|
||||
channel = self.get_channel_or_raise()
|
||||
return channel.wateringActive is False
|
||||
return self.functional_channel.wateringActive is False
|
||||
|
||||
@@ -287,11 +287,7 @@ class DashboardsCollection(collection.DictStorageCollection):
|
||||
raise vol.Invalid("Url path needs to contain a hyphen (-)")
|
||||
|
||||
if url_path in self.hass.data[DATA_PANELS]:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="url_already_exists",
|
||||
translation_placeholders={"url": url_path},
|
||||
)
|
||||
raise vol.Invalid("Panel url path needs to be unique")
|
||||
|
||||
return self.CREATE_SCHEMA(data) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"url_already_exists": {
|
||||
"message": "The URL \"{url}\" is already in use. Please choose a different one."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload_resources": {
|
||||
"description": "Reloads dashboard resources from the YAML-configuration.",
|
||||
|
||||
@@ -25,7 +25,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"scenes": bridge.scenes,
|
||||
"occupancy_groups": bridge.occupancy_groups,
|
||||
"areas": bridge.areas,
|
||||
"smart_away_state": bridge.smart_away_state,
|
||||
},
|
||||
"integration_data": {
|
||||
"keypad_button_names_to_leap": data.keypad_data.button_names_to_leap,
|
||||
|
||||
@@ -5,12 +5,9 @@ from typing import Any
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import LutronCasetaEntity, LutronCasetaUpdatableEntity
|
||||
from .models import LutronCasetaData
|
||||
from .entity import LutronCasetaUpdatableEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -26,14 +23,9 @@ async def async_setup_entry(
|
||||
data = config_entry.runtime_data
|
||||
bridge = data.bridge
|
||||
switch_devices = bridge.get_devices_by_domain(SWITCH_DOMAIN)
|
||||
entities: list[LutronCasetaLight | LutronCasetaSmartAwaySwitch] = [
|
||||
async_add_entities(
|
||||
LutronCasetaLight(switch_device, data) for switch_device in switch_devices
|
||||
]
|
||||
|
||||
if bridge.smart_away_state != "":
|
||||
entities.append(LutronCasetaSmartAwaySwitch(data))
|
||||
|
||||
async_add_entities(entities)
|
||||
)
|
||||
|
||||
|
||||
class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
|
||||
@@ -69,46 +61,3 @@ class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self._device["current_state"] > 0
|
||||
|
||||
|
||||
class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
|
||||
"""Representation of Lutron Caseta Smart Away."""
|
||||
|
||||
def __init__(self, data: LutronCasetaData) -> None:
|
||||
"""Init a switch entity."""
|
||||
device = {
|
||||
"device_id": "smart_away",
|
||||
"name": "Smart Away",
|
||||
"type": "SmartAway",
|
||||
"model": "Smart Away",
|
||||
"area": data.bridge_device["area"],
|
||||
"serial": data.bridge_device["serial"],
|
||||
}
|
||||
super().__init__(device, data)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, data.bridge_device["serial"])},
|
||||
)
|
||||
self._smart_away_unique_id = f"{self._bridge_unique_id}_smart_away"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of the smart away switch."""
|
||||
return self._smart_away_unique_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn Smart Away on."""
|
||||
await self._smartbridge.activate_smart_away()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn Smart Away off."""
|
||||
await self._smartbridge.deactivate_smart_away()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if Smart Away is on."""
|
||||
return self._smartbridge.smart_away_state == "Enabled"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0", "aiofiles==24.1.0"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0"]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ from homeassistant.util.async_ import create_eager_task
|
||||
from . import debug_info, discovery
|
||||
from .client import (
|
||||
MQTT,
|
||||
async_on_subscribe_done,
|
||||
async_publish,
|
||||
async_subscribe,
|
||||
async_subscribe_internal,
|
||||
@@ -164,7 +163,6 @@ __all__ = [
|
||||
"async_create_certificate_temp_files",
|
||||
"async_forward_entry_setup_and_setup_discovery",
|
||||
"async_migrate_entry",
|
||||
"async_on_subscribe_done",
|
||||
"async_prepare_subscribe_topics",
|
||||
"async_publish",
|
||||
"async_remove_config_entry_device",
|
||||
|
||||
@@ -38,10 +38,7 @@ from homeassistant.core import (
|
||||
get_hassjob_callable_job_type,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -74,7 +71,6 @@ from .const import (
|
||||
DEFAULT_WS_PATH,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTION_STATE,
|
||||
MQTT_PROCESSED_SUBSCRIPTIONS,
|
||||
PROTOCOL_5,
|
||||
PROTOCOL_31,
|
||||
TRANSPORT_WEBSOCKETS,
|
||||
@@ -113,7 +109,6 @@ INITIAL_SUBSCRIBE_COOLDOWN = 0.5
|
||||
SUBSCRIBE_COOLDOWN = 0.1
|
||||
UNSUBSCRIBE_COOLDOWN = 0.1
|
||||
TIMEOUT_ACK = 10
|
||||
SUBSCRIBE_TIMEOUT = 10
|
||||
RECONNECT_INTERVAL_SECONDS = 10
|
||||
|
||||
MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1
|
||||
@@ -189,38 +184,6 @@ async def async_publish(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_on_subscribe_done(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
qos: int,
|
||||
on_subscribe_status: CALLBACK_TYPE,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Call on_subscribe_done when the matched subscription was completed.
|
||||
|
||||
If a subscription is already present the callback will call
|
||||
on_subscribe_status directly.
|
||||
Call the returned callback to stop and cleanup status monitoring.
|
||||
"""
|
||||
|
||||
async def _sync_mqtt_subscribe(subscriptions: list[tuple[str, int]]) -> None:
|
||||
if (topic, qos) not in subscriptions:
|
||||
return
|
||||
hass.loop.call_soon(on_subscribe_status)
|
||||
|
||||
mqtt_data = hass.data[DATA_MQTT]
|
||||
if (
|
||||
mqtt_data.client.connected
|
||||
and mqtt_data.client.is_active_subscription(topic)
|
||||
and not mqtt_data.client.is_pending_subscription(topic)
|
||||
):
|
||||
hass.loop.call_soon(on_subscribe_status)
|
||||
|
||||
return async_dispatcher_connect(
|
||||
hass, MQTT_PROCESSED_SUBSCRIPTIONS, _sync_mqtt_subscribe
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_subscribe(
|
||||
hass: HomeAssistant,
|
||||
@@ -228,32 +191,12 @@ async def async_subscribe(
|
||||
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
|
||||
qos: int = DEFAULT_QOS,
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
on_subscribe: CALLBACK_TYPE | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to an MQTT topic.
|
||||
|
||||
If the on_subcribe callback hook is set, it will be called once
|
||||
when the subscription has been completed.
|
||||
|
||||
Call the return value to unsubscribe.
|
||||
"""
|
||||
handler: CALLBACK_TYPE | None = None
|
||||
|
||||
def _on_subscribe_done() -> None:
|
||||
"""Call once when the subscription was completed."""
|
||||
if TYPE_CHECKING:
|
||||
assert on_subscribe is not None and handler is not None
|
||||
|
||||
handler()
|
||||
on_subscribe()
|
||||
|
||||
subscription_handler = async_subscribe_internal(
|
||||
hass, topic, msg_callback, qos, encoding
|
||||
)
|
||||
if on_subscribe is not None:
|
||||
handler = async_on_subscribe_done(hass, topic, qos, _on_subscribe_done)
|
||||
|
||||
return subscription_handler
|
||||
return async_subscribe_internal(hass, topic, msg_callback, qos, encoding)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -697,16 +640,12 @@ class MQTT:
|
||||
if fileno > -1:
|
||||
self.loop.remove_writer(sock)
|
||||
|
||||
def is_active_subscription(self, topic: str) -> bool:
|
||||
def _is_active_subscription(self, topic: str) -> bool:
|
||||
"""Check if a topic has an active subscription."""
|
||||
return topic in self._simple_subscriptions or any(
|
||||
other.topic == topic for other in self._wildcard_subscriptions
|
||||
)
|
||||
|
||||
def is_pending_subscription(self, topic: str) -> bool:
|
||||
"""Check if a topic has a pending subscription."""
|
||||
return topic in self._pending_subscriptions
|
||||
|
||||
async def async_publish(
|
||||
self, topic: str, payload: PublishPayloadType, qos: int, retain: bool
|
||||
) -> None:
|
||||
@@ -960,7 +899,7 @@ class MQTT:
|
||||
@callback
|
||||
def _async_unsubscribe(self, topic: str) -> None:
|
||||
"""Unsubscribe from a topic."""
|
||||
if self.is_active_subscription(topic):
|
||||
if self._is_active_subscription(topic):
|
||||
if self._max_qos[topic] == 0:
|
||||
return
|
||||
subs = self._matching_subscriptions(topic)
|
||||
@@ -1024,7 +963,6 @@ class MQTT:
|
||||
self._last_subscribe = time.monotonic()
|
||||
|
||||
await self._async_wait_for_mid_or_raise(mid, result)
|
||||
async_dispatcher_send(self.hass, MQTT_PROCESSED_SUBSCRIPTIONS, chunk_list)
|
||||
|
||||
async def _async_perform_unsubscribes(self) -> None:
|
||||
"""Perform pending MQTT client unsubscribes."""
|
||||
|
||||
@@ -375,7 +375,6 @@ DOMAIN = "mqtt"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
MQTT_CONNECTION_STATE = "mqtt_connection_state"
|
||||
MQTT_PROCESSED_SUBSCRIPTIONS = "mqtt_processed_subscriptions"
|
||||
|
||||
PAYLOAD_EMPTY_JSON = "{}"
|
||||
PAYLOAD_NONE = "None"
|
||||
|
||||
@@ -306,14 +306,6 @@ class OllamaSubentryFlowHandler(ConfigSubentryFlow):
|
||||
async_step_reconfigure = async_step_set_options
|
||||
|
||||
|
||||
def filter_invalid_llm_apis(hass: HomeAssistant, selected_apis: list[str]) -> list[str]:
|
||||
"""Accepts a list of LLM API IDs and filters this against those currently available."""
|
||||
|
||||
valid_llm_apis = [api.id for api in llm.async_get_apis(hass)]
|
||||
|
||||
return [api for api in selected_apis if api in valid_llm_apis]
|
||||
|
||||
|
||||
def ollama_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
is_new: bool,
|
||||
@@ -334,10 +326,6 @@ def ollama_config_option_schema(
|
||||
else:
|
||||
schema = {}
|
||||
|
||||
selected_llm_apis = filter_invalid_llm_apis(
|
||||
hass, options.get(CONF_LLM_HASS_API, [])
|
||||
)
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -361,7 +349,7 @@ def ollama_config_option_schema(
|
||||
): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
description={"suggested_value": selected_llm_apis},
|
||||
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
|
||||
@@ -60,7 +60,6 @@ from .coordinator import (
|
||||
from .repairs import (
|
||||
async_manage_ble_scanner_firmware_unsupported_issue,
|
||||
async_manage_deprecated_firmware_issue,
|
||||
async_manage_open_wifi_ap_issue,
|
||||
async_manage_outbound_websocket_incorrectly_enabled_issue,
|
||||
)
|
||||
from .utils import (
|
||||
@@ -348,7 +347,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
hass,
|
||||
entry,
|
||||
)
|
||||
async_manage_open_wifi_ap_issue(hass, entry)
|
||||
remove_empty_sub_devices(hass, entry)
|
||||
elif (
|
||||
sleep_period is None
|
||||
|
||||
@@ -5,18 +5,14 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Mapping
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioshelly.ble import get_name_from_model_id
|
||||
from aioshelly.ble.manufacturer_data import (
|
||||
has_rpc_over_ble,
|
||||
parse_shelly_manufacturer_data,
|
||||
)
|
||||
from aioshelly.ble.provisioning import (
|
||||
async_provision_wifi,
|
||||
async_scan_wifi_networks,
|
||||
ble_rpc_device,
|
||||
)
|
||||
from aioshelly.ble.provisioning import async_provision_wifi, async_scan_wifi_networks
|
||||
from aioshelly.block_device import BlockDevice
|
||||
from aioshelly.common import ConnectionOptions, get_info
|
||||
from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS
|
||||
@@ -37,7 +33,6 @@ from homeassistant.components import zeroconf
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_ble_device_from_address,
|
||||
async_clear_address_from_match_history,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import (
|
||||
@@ -103,32 +98,6 @@ BLE_SCANNER_OPTIONS = [
|
||||
|
||||
INTERNAL_WIFI_AP_IP = "192.168.33.1"
|
||||
|
||||
|
||||
async def async_get_ip_from_ble(ble_device: BLEDevice) -> str | None:
|
||||
"""Get device IP address via BLE after WiFi provisioning.
|
||||
|
||||
Args:
|
||||
ble_device: BLE device to query
|
||||
|
||||
Returns:
|
||||
IP address string if available, None otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
async with ble_rpc_device(ble_device) as device:
|
||||
await device.update_status()
|
||||
if (
|
||||
(wifi := device.status.get("wifi"))
|
||||
and isinstance(wifi, dict)
|
||||
and (ip := wifi.get("sta_ip"))
|
||||
):
|
||||
return cast(str, ip)
|
||||
return None
|
||||
except (DeviceConnectionError, RpcCallError) as err:
|
||||
LOGGER.debug("Failed to get IP via BLE: %s", err)
|
||||
return None
|
||||
|
||||
|
||||
# BLE provisioning flow steps that are in the finishing state
|
||||
# Used to determine if a BLE flow should be aborted when zeroconf discovers the device
|
||||
BLUETOOTH_FINISHING_STEPS = {"do_provision", "provision_done"}
|
||||
@@ -426,14 +395,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not mac:
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
|
||||
# Clear match history at the start of discovery flow.
|
||||
# This ensures that if the user never provisions the device and it
|
||||
# disappears (powers down), the discovery flow gets cleaned up,
|
||||
# and then the device comes back later, it can be rediscovered.
|
||||
# Also handles factory reset scenarios where the device may reappear
|
||||
# with different advertisement content (RPC-over-BLE re-enabled).
|
||||
async_clear_address_from_match_history(self.hass, discovery_info.address)
|
||||
|
||||
# Check if RPC-over-BLE is enabled - required for WiFi provisioning
|
||||
if not has_rpc_over_ble(discovery_info.manufacturer_data):
|
||||
LOGGER.debug(
|
||||
@@ -678,21 +639,13 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
aiozc = await zeroconf.async_get_async_instance(self.hass)
|
||||
result = await async_lookup_device_by_name(aiozc, self.device_name)
|
||||
|
||||
# If we still don't have a host, try BLE fallback for alternate subnets
|
||||
# If we still don't have a host, provisioning failed
|
||||
if not result:
|
||||
LOGGER.debug(
|
||||
"Active lookup failed, trying to get IP address via BLE as fallback"
|
||||
)
|
||||
if ip := await async_get_ip_from_ble(self.ble_device):
|
||||
LOGGER.debug("Got IP %s from BLE, using it", ip)
|
||||
state.host = ip
|
||||
state.port = DEFAULT_HTTP_PORT
|
||||
else:
|
||||
LOGGER.debug("BLE fallback also failed - provisioning unsuccessful")
|
||||
# Store failure info and return None - provision_done will handle redirect
|
||||
return None
|
||||
else:
|
||||
state.host, state.port = result
|
||||
LOGGER.debug("Active lookup failed - provisioning unsuccessful")
|
||||
# Store failure info and return None - provision_done will handle redirect
|
||||
return None
|
||||
|
||||
state.host, state.port = result
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Zeroconf discovery received for device after WiFi provisioning at %s",
|
||||
@@ -732,13 +685,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# Secure device after provisioning if requested (disable AP/BLE)
|
||||
await self._async_secure_device_after_provision(self.host, self.port)
|
||||
|
||||
# Clear match history so device can be rediscovered if factory reset
|
||||
# This ensures that if the device is factory reset in the future
|
||||
# (re-enabling BLE provisioning), it will trigger a new discovery flow
|
||||
if TYPE_CHECKING:
|
||||
assert self.ble_device is not None
|
||||
async_clear_address_from_match_history(self.hass, self.ble_device.address)
|
||||
|
||||
# User just provisioned this device - create entry directly without confirmation
|
||||
return self.async_create_entry(
|
||||
title=device_info["title"],
|
||||
|
||||
@@ -254,7 +254,6 @@ OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID = (
|
||||
"outbound_websocket_incorrectly_enabled_{unique}"
|
||||
)
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID = "deprecated_firmware_{unique}"
|
||||
OPEN_WIFI_AP_ISSUE_ID = "open_wifi_ap_{unique}"
|
||||
|
||||
|
||||
class DeprecatedFirmwareInfo(TypedDict):
|
||||
|
||||
@@ -20,14 +20,14 @@ from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER, ROLE_GENERIC
|
||||
from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
get_block_device_info,
|
||||
get_rpc_channel_name,
|
||||
get_block_entity_name,
|
||||
get_rpc_device_info,
|
||||
get_rpc_key,
|
||||
get_rpc_entity_name,
|
||||
get_rpc_key_instances,
|
||||
get_rpc_role_by_key,
|
||||
)
|
||||
@@ -371,7 +371,7 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
|
||||
"""Initialize Shelly entity."""
|
||||
super().__init__(coordinator)
|
||||
self.block = block
|
||||
|
||||
self._attr_name = get_block_entity_name(coordinator.device, block)
|
||||
self._attr_device_info = get_entity_block_device_info(coordinator, block)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{block.description}"
|
||||
|
||||
@@ -413,9 +413,9 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
|
||||
"""Initialize Shelly entity."""
|
||||
super().__init__(coordinator)
|
||||
self.key = key
|
||||
|
||||
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{key}"
|
||||
self._attr_name = get_rpc_entity_name(coordinator.device, key)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -467,6 +467,9 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity):
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id: str = f"{super().unique_id}-{self.attribute}"
|
||||
self._attr_name = get_block_entity_name(
|
||||
coordinator.device, block, description.name
|
||||
)
|
||||
|
||||
@property
|
||||
def attribute_value(self) -> StateType:
|
||||
@@ -504,7 +507,9 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
|
||||
self.block_coordinator = coordinator
|
||||
self.attribute = attribute
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_name = get_block_entity_name(
|
||||
coordinator.device, None, description.name
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{attribute}"
|
||||
self._attr_device_info = get_entity_block_device_info(coordinator)
|
||||
self._last_value = None
|
||||
@@ -541,13 +546,13 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, Entity):
|
||||
self.attribute = attribute
|
||||
self.entity_description = description
|
||||
|
||||
if description.role == ROLE_GENERIC:
|
||||
self._attr_name = get_rpc_channel_name(coordinator.device, key)
|
||||
|
||||
self._attr_unique_id = f"{super().unique_id}-{attribute}"
|
||||
self._attr_name = get_rpc_entity_name(
|
||||
coordinator.device, key, description.name, description.role
|
||||
)
|
||||
self._last_value = None
|
||||
has_id, _, component_id = get_rpc_key(key)
|
||||
self._id = int(component_id) if has_id and component_id.isnumeric() else None
|
||||
id_key = key.split(":")[-1]
|
||||
self._id = int(id_key) if id_key.isnumeric() else None
|
||||
|
||||
if description.unit is not None:
|
||||
self._attr_native_unit_of_measurement = description.unit(
|
||||
@@ -621,6 +626,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
|
||||
self._attr_unique_id = (
|
||||
f"{self.coordinator.mac}-{block.description}-{attribute}"
|
||||
)
|
||||
self._attr_name = get_block_entity_name(
|
||||
coordinator.device, block, description.name
|
||||
)
|
||||
elif entry is not None:
|
||||
self._attr_unique_id = entry.unique_id
|
||||
|
||||
@@ -681,7 +689,11 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity):
|
||||
self._attr_unique_id = f"{coordinator.mac}-{key}-{attribute}"
|
||||
self._last_value = None
|
||||
|
||||
if not coordinator.device.initialized and entry is not None:
|
||||
if coordinator.device.initialized:
|
||||
self._attr_name = get_rpc_entity_name(
|
||||
coordinator.device, key, description.name
|
||||
)
|
||||
elif entry is not None:
|
||||
self._attr_name = cast(str, entry.original_name)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==13.21.0"],
|
||||
"requirements": ["aioshelly==13.20.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "shelly*",
|
||||
|
||||
@@ -22,7 +22,6 @@ from .const import (
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID,
|
||||
DEPRECATED_FIRMWARES,
|
||||
DOMAIN,
|
||||
OPEN_WIFI_AP_ISSUE_ID,
|
||||
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID,
|
||||
BLEScannerMode,
|
||||
)
|
||||
@@ -150,45 +149,6 @@ def async_manage_outbound_websocket_incorrectly_enabled_issue(
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_manage_open_wifi_ap_issue(
|
||||
hass: HomeAssistant,
|
||||
entry: ShellyConfigEntry,
|
||||
) -> None:
|
||||
"""Manage the open WiFi AP issue."""
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=entry.unique_id)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert entry.runtime_data.rpc is not None
|
||||
|
||||
device = entry.runtime_data.rpc.device
|
||||
|
||||
# Check if WiFi AP is enabled and is open (no password)
|
||||
if (
|
||||
(wifi_config := device.config.get("wifi"))
|
||||
and (ap_config := wifi_config.get("ap"))
|
||||
and ap_config.get("enable")
|
||||
and ap_config.get("is_open")
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="open_wifi_ap",
|
||||
translation_placeholders={
|
||||
"device_name": device.name,
|
||||
"ip_address": device.ip_address,
|
||||
},
|
||||
data={"entry_id": entry.entry_id},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
class ShellyRpcRepairsFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
@@ -269,49 +229,6 @@ class DisableOutboundWebSocketFlow(ShellyRpcRepairsFlow):
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
|
||||
class DisableOpenWiFiApFlow(RepairsFlow):
|
||||
"""Handler for Disable Open WiFi AP flow."""
|
||||
|
||||
def __init__(self, device: RpcDevice, issue_id: str) -> None:
|
||||
"""Initialize."""
|
||||
self._device = device
|
||||
self.issue_id = issue_id
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
issue_registry = ir.async_get(self.hass)
|
||||
description_placeholders = None
|
||||
if issue := issue_registry.async_get_issue(DOMAIN, self.issue_id):
|
||||
description_placeholders = issue.translation_placeholders
|
||||
|
||||
return self.async_show_menu(
|
||||
menu_options=["confirm", "ignore"],
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
try:
|
||||
result = await self._device.wifi_setconfig(ap_enable=False)
|
||||
if result.get("restart_required"):
|
||||
await self._device.trigger_reboot()
|
||||
except (DeviceConnectionError, RpcCallError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
async def async_step_ignore(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the ignore step of a fix flow."""
|
||||
ir.async_ignore_issue(self.hass, DOMAIN, self.issue_id, True)
|
||||
return self.async_abort(reason="issue_ignored")
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
||||
) -> RepairsFlow:
|
||||
@@ -336,7 +253,4 @@ async def async_create_fix_flow(
|
||||
if "outbound_websocket_incorrectly_enabled" in issue_id:
|
||||
return DisableOutboundWebSocketFlow(device)
|
||||
|
||||
if "open_wifi_ap" in issue_id:
|
||||
return DisableOpenWiFiApFlow(device, issue_id)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
|
||||
@@ -59,6 +59,9 @@ class RpcSelect(ShellyRpcAttributeEntity, SelectEntity):
|
||||
if self.option_map:
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
if hasattr(self, "_attr_name") and description.role != ROLE_GENERIC:
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
|
||||
@@ -664,25 +664,6 @@
|
||||
"description": "Shelly device {device_name} with IP address {ip_address} requires calibration. To calibrate the device, it must be rebooted after proper installation on the valve. You can reboot the device in its web panel, go to 'Settings' > 'Device Reboot'.",
|
||||
"title": "Shelly device {device_name} is not calibrated"
|
||||
},
|
||||
"open_wifi_ap": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"issue_ignored": "Issue ignored"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Your Shelly device {device_name} with IP address {ip_address} has an open WiFi access point enabled without a password. This is a security risk as anyone nearby can connect to the device.\n\nNote: If you disable the access point, the device may need to restart.",
|
||||
"menu_options": {
|
||||
"confirm": "Disable WiFi access point",
|
||||
"ignore": "Ignore"
|
||||
},
|
||||
"title": "[%key:component::shelly::issues::open_wifi_ap::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Open WiFi access point on {device_name}"
|
||||
},
|
||||
"outbound_websocket_incorrectly_enabled": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
|
||||
@@ -49,6 +49,7 @@ from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
)
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
@@ -116,6 +117,20 @@ def get_block_number_of_channels(device: BlockDevice, block: Block) -> int:
|
||||
return channels or 1
|
||||
|
||||
|
||||
def get_block_entity_name(
|
||||
device: BlockDevice,
|
||||
block: Block | None,
|
||||
name: str | UndefinedType | None = None,
|
||||
) -> str | None:
|
||||
"""Naming for block based switch and sensors."""
|
||||
channel_name = get_block_channel_name(device, block)
|
||||
|
||||
if name is not UNDEFINED and name:
|
||||
return f"{channel_name} {name.lower()}" if channel_name else name
|
||||
|
||||
return channel_name
|
||||
|
||||
|
||||
def get_block_custom_name(device: BlockDevice, block: Block | None) -> str | None:
|
||||
"""Get custom name from device settings."""
|
||||
if block and (key := cast(str, block.type) + "s") and key in device.settings:
|
||||
@@ -459,6 +474,23 @@ def get_rpc_sub_device_name(
|
||||
return f"{device.name} {component.title()} {component_id}"
|
||||
|
||||
|
||||
def get_rpc_entity_name(
|
||||
device: RpcDevice,
|
||||
key: str,
|
||||
name: str | UndefinedType | None = None,
|
||||
role: str | None = None,
|
||||
) -> str | None:
|
||||
"""Naming for RPC based switch and sensors."""
|
||||
channel_name = get_rpc_channel_name(device, key)
|
||||
|
||||
if name is not UNDEFINED and name:
|
||||
if role and role != ROLE_GENERIC:
|
||||
return name
|
||||
return f"{channel_name} {name.lower()}" if channel_name else name
|
||||
|
||||
return channel_name
|
||||
|
||||
|
||||
def get_entity_translation_attributes(
|
||||
channel_name: str | None,
|
||||
translation_key: str | None,
|
||||
@@ -794,9 +826,11 @@ async def get_rpc_scripts_event_types(
|
||||
device: RpcDevice, ignore_scripts: list[str]
|
||||
) -> dict[int, list[str]]:
|
||||
"""Return a dict of all scripts and their event types."""
|
||||
script_instances = get_rpc_key_instances(device.status, "script")
|
||||
script_events = {}
|
||||
for script in get_rpc_key_instances(device.status, "script"):
|
||||
if get_rpc_channel_name(device, script) in ignore_scripts:
|
||||
for script in script_instances:
|
||||
script_name = get_rpc_entity_name(device, script)
|
||||
if script_name in ignore_scripts:
|
||||
continue
|
||||
|
||||
script_id = get_rpc_key_id(script)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["slack"],
|
||||
"requirements": ["slack_sdk==3.33.4", "aiofiles==24.1.0"]
|
||||
"requirements": ["slack_sdk==3.33.4"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.const import (
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -165,15 +164,6 @@ WALL_CONNECTOR_SENSORS = [
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="total_power_w",
|
||||
translation_key="total_power_w",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].total_power_w,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="session_energy_wh",
|
||||
translation_key="session_energy_wh",
|
||||
|
||||
@@ -75,9 +75,6 @@
|
||||
"status_code": {
|
||||
"name": "Status code"
|
||||
},
|
||||
"total_power_w": {
|
||||
"name": "Total power"
|
||||
},
|
||||
"voltage_a_v": {
|
||||
"name": "Phase A voltage"
|
||||
},
|
||||
|
||||
@@ -123,9 +123,7 @@ async def async_setup_entry(
|
||||
ThermoProBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class ThermoProBluetoothSensorEntity(
|
||||
|
||||
@@ -22,7 +22,6 @@ ATTR_FOLLOW_SINCE = "following_since"
|
||||
ATTR_FOLLOWING = "followers"
|
||||
ATTR_VIEWERS = "viewers"
|
||||
ATTR_STARTED_AT = "started_at"
|
||||
ATTR_CHANNEL_PICTURE = "channel_picture"
|
||||
|
||||
STATE_OFFLINE = "offline"
|
||||
STATE_STREAMING = "streaming"
|
||||
@@ -83,7 +82,6 @@ class TwitchSensor(CoordinatorEntity[TwitchCoordinator], SensorEntity):
|
||||
ATTR_STARTED_AT: channel.started_at,
|
||||
ATTR_VIEWERS: channel.viewers,
|
||||
ATTR_SUBSCRIPTION: False,
|
||||
ATTR_CHANNEL_PICTURE: channel.picture,
|
||||
}
|
||||
if channel.subscribed is not None:
|
||||
resp[ATTR_SUBSCRIPTION] = channel.subscribed
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.core import (
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
@@ -46,7 +47,6 @@ from .const import (
|
||||
VEHICLE_TYPES,
|
||||
)
|
||||
from .coordinator import WazeTravelTimeCoordinator, async_get_travel_times
|
||||
from .httpx_client import create_httpx_client
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -106,8 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}):
|
||||
hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1)
|
||||
|
||||
httpx_client = await create_httpx_client(hass)
|
||||
|
||||
httpx_client = get_async_client(hass)
|
||||
client = WazeRouteCalculator(
|
||||
region=config_entry.data[CONF_REGION].upper(), client=httpx_client
|
||||
)
|
||||
@@ -120,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
async def async_get_travel_times_service(service: ServiceCall) -> ServiceResponse:
|
||||
httpx_client = await create_httpx_client(hass)
|
||||
httpx_client = get_async_client(hass)
|
||||
client = WazeRouteCalculator(
|
||||
region=service.data[CONF_REGION].upper(), client=httpx_client
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""Special httpx client for Waze Travel Time integration."""
|
||||
|
||||
import httpx
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_HTTPX_ASYNC_CLIENT: HassKey[httpx.AsyncClient] = HassKey("httpx_async_client")
|
||||
|
||||
|
||||
def create_transport() -> httpx.AsyncHTTPTransport:
|
||||
"""Create a httpx transport which enforces the use of IPv4."""
|
||||
return httpx.AsyncHTTPTransport(local_address="0.0.0.0")
|
||||
|
||||
|
||||
async def create_httpx_client(hass: HomeAssistant) -> httpx.AsyncClient:
|
||||
"""Create a httpx client which enforces the use of IPv4."""
|
||||
if (client := hass.data[DOMAIN].get(DATA_HTTPX_ASYNC_CLIENT)) is None:
|
||||
transport = await hass.async_add_executor_job(create_transport)
|
||||
client = hass.data[DOMAIN][DATA_HTTPX_ASYNC_CLIENT] = create_async_httpx_client(
|
||||
hass, transport=transport
|
||||
)
|
||||
return client
|
||||
@@ -99,13 +99,10 @@ class WizBulbEntity(WizToggleEntity, LightEntity):
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Handle updating _attr values."""
|
||||
state = self._device.state
|
||||
|
||||
if (brightness := state.get_brightness()) is not None:
|
||||
self._attr_brightness = max(0, min(255, brightness))
|
||||
|
||||
color_modes = self.supported_color_modes
|
||||
assert color_modes is not None
|
||||
|
||||
if (brightness := state.get_brightness()) is not None:
|
||||
self._attr_brightness = max(0, min(255, brightness))
|
||||
if ColorMode.COLOR_TEMP in color_modes and (
|
||||
color_temp := state.get_colortemp()
|
||||
):
|
||||
@@ -114,19 +111,12 @@ class WizBulbEntity(WizToggleEntity, LightEntity):
|
||||
elif (
|
||||
ColorMode.RGBWW in color_modes and (rgbww := state.get_rgbww()) is not None
|
||||
):
|
||||
self._attr_color_mode = ColorMode.RGBWW
|
||||
self._attr_rgbww_color = rgbww
|
||||
self._attr_color_mode = ColorMode.RGBWW
|
||||
elif ColorMode.RGBW in color_modes and (rgbw := state.get_rgbw()) is not None:
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
self._attr_rgbw_color = rgbw
|
||||
|
||||
self._attr_effect = effect = state.get_scene()
|
||||
if effect is not None:
|
||||
if brightness is not None:
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
else:
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
self._attr_effect = state.get_scene()
|
||||
super()._async_update_attrs()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -157,7 +157,6 @@ FLOWS = {
|
||||
"droplet",
|
||||
"dsmr",
|
||||
"dsmr_reader",
|
||||
"duckdns",
|
||||
"duke_energy",
|
||||
"dunehd",
|
||||
"duotecno",
|
||||
|
||||
@@ -1467,7 +1467,7 @@
|
||||
"duckdns": {
|
||||
"name": "Duck DNS",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"duke_energy": {
|
||||
|
||||
@@ -55,7 +55,11 @@ from homeassistant.core import (
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import entity_registry as er, location as loc_helper
|
||||
from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
location as loc_helper,
|
||||
)
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.translation import async_translate_state
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
@@ -1219,6 +1223,25 @@ def config_entry_attr(
|
||||
return getattr(config_entry, attr_name)
|
||||
|
||||
|
||||
def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Return all open issues."""
|
||||
current_issues = ir.async_get(hass).issues
|
||||
# Use JSON for safe representation
|
||||
return {
|
||||
key: issue_entry.to_json()
|
||||
for (key, issue_entry) in current_issues.items()
|
||||
if issue_entry.active
|
||||
}
|
||||
|
||||
|
||||
def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Get issue by domain and issue_id."""
|
||||
result = ir.async_get(hass).async_get_issue(domain, issue_id)
|
||||
if result:
|
||||
return result.to_json()
|
||||
return None
|
||||
|
||||
|
||||
def closest(hass: HomeAssistant, *args: Any) -> State | None:
|
||||
"""Find closest entity.
|
||||
|
||||
@@ -1873,7 +1896,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.IssuesExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
||||
@@ -1960,6 +1982,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.globals["config_entry_id"] = hassfunction(config_entry_id)
|
||||
self.filters["config_entry_id"] = self.globals["config_entry_id"]
|
||||
|
||||
# Issue extensions
|
||||
|
||||
self.globals["issues"] = hassfunction(issues)
|
||||
self.globals["issue"] = hassfunction(issue)
|
||||
self.filters["issue"] = self.globals["issue"]
|
||||
|
||||
if limited:
|
||||
# Only device_entities is available to limited templates, mark other
|
||||
# functions and filters as unsupported.
|
||||
|
||||
@@ -7,7 +7,6 @@ from .crypto import CryptoExtension
|
||||
from .datetime import DateTimeExtension
|
||||
from .devices import DeviceExtension
|
||||
from .floors import FloorExtension
|
||||
from .issues import IssuesExtension
|
||||
from .labels import LabelExtension
|
||||
from .math import MathExtension
|
||||
from .regex import RegexExtension
|
||||
@@ -21,7 +20,6 @@ __all__ = [
|
||||
"DateTimeExtension",
|
||||
"DeviceExtension",
|
||||
"FloorExtension",
|
||||
"IssuesExtension",
|
||||
"LabelExtension",
|
||||
"MathExtension",
|
||||
"RegexExtension",
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Issue functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .base import BaseTemplateExtension, TemplateFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.template import TemplateEnvironment
|
||||
|
||||
|
||||
class IssuesExtension(BaseTemplateExtension):
|
||||
"""Extension for issue-related template functions."""
|
||||
|
||||
def __init__(self, environment: TemplateEnvironment) -> None:
|
||||
"""Initialize the issues extension."""
|
||||
super().__init__(
|
||||
environment,
|
||||
functions=[
|
||||
TemplateFunction(
|
||||
"issues",
|
||||
self.issues,
|
||||
as_global=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"issue",
|
||||
self.issue,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def issues(self) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Return all open issues."""
|
||||
current_issues = ir.async_get(self.hass).issues
|
||||
# Use JSON for safe representation
|
||||
return {
|
||||
key: issue_entry.to_json()
|
||||
for (key, issue_entry) in current_issues.items()
|
||||
if issue_entry.active
|
||||
}
|
||||
|
||||
def issue(self, domain: str, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Get issue by domain and issue_id."""
|
||||
result = ir.async_get(self.hass).async_get_issue(domain, issue_id)
|
||||
if result:
|
||||
return result.to_json()
|
||||
return None
|
||||
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@@ -254,10 +254,6 @@ aioemonitor==1.0.5
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==42.7.0
|
||||
|
||||
# homeassistant.components.matrix
|
||||
# homeassistant.components.slack
|
||||
aiofiles==24.1.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
||||
@@ -393,7 +389,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.21.0
|
||||
aioshelly==13.20.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
|
||||
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@@ -242,10 +242,6 @@ aioemonitor==1.0.5
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==42.7.0
|
||||
|
||||
# homeassistant.components.matrix
|
||||
# homeassistant.components.slack
|
||||
aiofiles==24.1.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
||||
@@ -375,7 +371,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.21.0
|
||||
aioshelly==13.20.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
|
||||
@@ -48,6 +48,18 @@ async def mock_adguard() -> AsyncMock:
|
||||
|
||||
# async method mocks
|
||||
adguard_mock.version = AsyncMock(return_value="v0.107.50")
|
||||
adguard_mock.protection_enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.enable_protection = AsyncMock()
|
||||
adguard_mock.disable_protection = AsyncMock()
|
||||
adguard_mock.parental.enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.parental.enable = AsyncMock()
|
||||
adguard_mock.parental.disable = AsyncMock()
|
||||
adguard_mock.safesearch.enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.safesearch.enable = AsyncMock()
|
||||
adguard_mock.safesearch.disable = AsyncMock()
|
||||
adguard_mock.safebrowsing.enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.safebrowsing.enable = AsyncMock()
|
||||
adguard_mock.safebrowsing.disable = AsyncMock()
|
||||
adguard_mock.stats.dns_queries = AsyncMock(return_value=666)
|
||||
adguard_mock.stats.blocked_filtering = AsyncMock(return_value=1337)
|
||||
adguard_mock.stats.blocked_percentage = AsyncMock(return_value=200.75)
|
||||
@@ -61,6 +73,12 @@ async def mock_adguard() -> AsyncMock:
|
||||
adguard_mock.filtering.enable_url = AsyncMock()
|
||||
adguard_mock.filtering.disable_url = AsyncMock()
|
||||
adguard_mock.filtering.refresh = AsyncMock()
|
||||
adguard_mock.filtering.enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.filtering.enable = AsyncMock()
|
||||
adguard_mock.filtering.disable = AsyncMock()
|
||||
adguard_mock.querylog.enabled = AsyncMock(return_value=True)
|
||||
adguard_mock.querylog.enable = AsyncMock()
|
||||
adguard_mock.querylog.disable = AsyncMock()
|
||||
adguard_mock.update.update_available = AsyncMock(
|
||||
return_value=AdGuardHomeAvailableUpdate(
|
||||
new_version="v0.107.59",
|
||||
|
||||
289
tests/components/adguard/snapshots/test_switch.ambr
Normal file
289
tests/components/adguard/snapshots/test_switch.ambr
Normal file
@@ -0,0 +1,289 @@
|
||||
# serializer version: 1
|
||||
# name: test_switch[switch.adguard_home_filtering-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_filtering',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Filtering',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'filtering',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_filtering',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_filtering-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Filtering',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_filtering',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_parental_control-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_parental_control',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Parental control',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'parental',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_parental',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_parental_control-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Parental control',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_parental_control',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_protection-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_protection',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Protection',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'protection',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_protection',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_protection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Protection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_protection',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_query_log-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_query_log',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Query log',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'query_log',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_querylog',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_query_log-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Query log',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_query_log',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_safe_browsing-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_safe_browsing',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Safe browsing',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'safe_browsing',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_safebrowsing',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_safe_browsing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe browsing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_safe_browsing',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_safe_search-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.adguard_home_safe_search',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Safe search',
|
||||
'platform': 'adguard',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'safe_search',
|
||||
'unique_id': 'adguard_127.0.0.1_3000_switch_safesearch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switch[switch.adguard_home_safe_search-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'AdGuard Home Safe search',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.adguard_home_safe_search',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
161
tests/components/adguard/test_switch.py
Normal file
161
tests/components/adguard/test_switch.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Tests for the AdGuard Home switch entity."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from adguardhome import AdGuardHomeError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_switch(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the adguard switch platform."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize(
|
||||
("switch_name", "service", "call_assertion"),
|
||||
[
|
||||
(
|
||||
"protection",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.enable_protection.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"protection",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.disable_protection.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"parental_control",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.parental.enable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"parental_control",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.parental.disable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"safe_search",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.safesearch.enable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"safe_search",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.safesearch.disable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"safe_browsing",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.safebrowsing.enable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"safe_browsing",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.safebrowsing.disable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"filtering",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.filtering.enable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"filtering",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.filtering.disable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"query_log",
|
||||
SERVICE_TURN_ON,
|
||||
lambda mock: mock.querylog.enable.assert_called_once(),
|
||||
),
|
||||
(
|
||||
"query_log",
|
||||
SERVICE_TURN_OFF,
|
||||
lambda mock: mock.querylog.disable.assert_called_once(),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_actions(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
switch_name: str,
|
||||
service: str,
|
||||
call_assertion: Callable[[AsyncMock], Any],
|
||||
) -> None:
|
||||
"""Test the adguard switch actions."""
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
service,
|
||||
{ATTR_ENTITY_ID: f"switch.adguard_home_{switch_name}"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
call_assertion(mock_adguard)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_message"),
|
||||
[
|
||||
(
|
||||
SERVICE_TURN_ON,
|
||||
"An error occurred while turning on AdGuard Home switch",
|
||||
),
|
||||
(
|
||||
SERVICE_TURN_OFF,
|
||||
"An error occurred while turning off AdGuard Home switch",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_action_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_adguard: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
service: str,
|
||||
expected_message: str,
|
||||
) -> None:
|
||||
"""Test the adguard switch actions."""
|
||||
caplog.set_level(logging.ERROR)
|
||||
|
||||
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry, mock_adguard)
|
||||
|
||||
mock_adguard.enable_protection.side_effect = AdGuardHomeError("Boom")
|
||||
mock_adguard.disable_protection.side_effect = AdGuardHomeError("Boom")
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "switch.adguard_home_protection"},
|
||||
blocking=True,
|
||||
)
|
||||
assert expected_message in caplog.text
|
||||
@@ -34,7 +34,6 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import (
|
||||
TEST_DATA_CREATE_ENTRY,
|
||||
TEST_DATA_CREATE_ENTRY_2,
|
||||
TEST_DATA_CREATE_ENTRY_3,
|
||||
TEST_FRIENDLY_NAME,
|
||||
TEST_FRIENDLY_NAME_3,
|
||||
TEST_FRIENDLY_NAME_4,
|
||||
@@ -45,10 +44,8 @@ from .const import (
|
||||
TEST_JID_4,
|
||||
TEST_NAME,
|
||||
TEST_NAME_2,
|
||||
TEST_NAME_3,
|
||||
TEST_SERIAL_NUMBER,
|
||||
TEST_SERIAL_NUMBER_2,
|
||||
TEST_SERIAL_NUMBER_3,
|
||||
TEST_SOUND_MODE,
|
||||
TEST_SOUND_MODE_2,
|
||||
TEST_SOUND_MODE_NAME,
|
||||
@@ -79,17 +76,6 @@ def mock_config_entry_core() -> MockConfigEntry:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry_premiere() -> MockConfigEntry:
|
||||
"""Mock config entry for Beosound Premiere."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_SERIAL_NUMBER_3,
|
||||
data=TEST_DATA_CREATE_ENTRY_3,
|
||||
title=TEST_NAME_3,
|
||||
)
|
||||
|
||||
|
||||
async def mock_websocket_connection(
|
||||
hass: HomeAssistant, mock_mozart_client: AsyncMock
|
||||
) -> None:
|
||||
|
||||
@@ -39,7 +39,6 @@ TEST_HOST_INVALID = "192.168.0"
|
||||
TEST_HOST_IPV6 = "1111:2222:3333:4444:5555:6666:7777:8888"
|
||||
TEST_MODEL_BALANCE = "Beosound Balance"
|
||||
TEST_MODEL_CORE = "Beoconnect Core"
|
||||
TEST_MODEL_PREMIERE = "Beosound Premiere"
|
||||
TEST_MODEL_THEATRE = "Beosound Theatre"
|
||||
TEST_MODEL_LEVEL = "Beosound Level"
|
||||
TEST_SERIAL_NUMBER = "11111111"
|
||||
@@ -57,11 +56,9 @@ TEST_JID_2 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.{TEST_SERIAL_NUMBER_2}@prod
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_2 = "media_player.beoconnect_core_22222222"
|
||||
TEST_HOST_2 = "192.168.0.2"
|
||||
|
||||
TEST_FRIENDLY_NAME_3 = "Bedroom Premiere"
|
||||
TEST_SERIAL_NUMBER_3 = "33333333"
|
||||
TEST_NAME_3 = f"{TEST_MODEL_PREMIERE}-{TEST_SERIAL_NUMBER_3}"
|
||||
TEST_JID_3 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.{TEST_SERIAL_NUMBER_3}@products.bang-olufsen.com"
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_3 = f"media_player.beosound_premiere_{TEST_SERIAL_NUMBER_3}"
|
||||
TEST_FRIENDLY_NAME_3 = "Lego room Balance"
|
||||
TEST_JID_3 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.33333333@products.bang-olufsen.com"
|
||||
TEST_MEDIA_PLAYER_ENTITY_ID_3 = "media_player.beosound_balance_33333333"
|
||||
TEST_HOST_3 = "192.168.0.3"
|
||||
|
||||
TEST_FRIENDLY_NAME_4 = "Lounge room Balance"
|
||||
@@ -93,13 +90,6 @@ TEST_DATA_CREATE_ENTRY_2 = {
|
||||
CONF_NAME: TEST_NAME_2,
|
||||
}
|
||||
|
||||
TEST_DATA_CREATE_ENTRY_3 = {
|
||||
CONF_HOST: TEST_HOST_3,
|
||||
CONF_MODEL: TEST_MODEL_PREMIERE,
|
||||
CONF_BEOLINK_JID: TEST_JID_3,
|
||||
CONF_NAME: TEST_NAME_3,
|
||||
}
|
||||
|
||||
TEST_DATA_ZEROCONF = ZeroconfServiceInfo(
|
||||
ip_address=IPv4Address(TEST_HOST),
|
||||
ip_addresses=[IPv4Address(TEST_HOST)],
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
'attributes': dict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_button_event_creation_balance
|
||||
# name: test_button_event_creation
|
||||
list([
|
||||
'event.beosound_balance_11111111_bluetooth',
|
||||
'event.beosound_balance_11111111_microphone',
|
||||
@@ -19,17 +19,3 @@
|
||||
'media_player.beoconnect_core_22222222',
|
||||
])
|
||||
# ---
|
||||
# name: test_button_event_creation_beosound_premiere
|
||||
list([
|
||||
'event.beosound_premiere_33333333_microphone',
|
||||
'event.beosound_premiere_33333333_next',
|
||||
'event.beosound_premiere_33333333_play_pause',
|
||||
'event.beosound_premiere_33333333_favorite_1',
|
||||
'event.beosound_premiere_33333333_favorite_2',
|
||||
'event.beosound_premiere_33333333_favorite_3',
|
||||
'event.beosound_premiere_33333333_favorite_4',
|
||||
'event.beosound_premiere_33333333_previous',
|
||||
'event.beosound_premiere_33333333_volume',
|
||||
'media_player.beosound_premiere_33333333',
|
||||
])
|
||||
# ---
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -52,11 +52,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -101,11 +101,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -150,11 +150,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -199,11 +199,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -248,11 +248,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -296,11 +296,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -344,11 +344,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -392,11 +392,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -440,11 +440,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -488,11 +488,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -536,11 +536,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -584,11 +584,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -633,11 +633,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -682,11 +682,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -731,11 +731,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -780,11 +780,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -830,11 +830,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -879,11 +879,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -928,11 +928,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -977,11 +977,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -1028,7 +1028,7 @@
|
||||
'Laundry room Core': '1111.1111111.22222222@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
@@ -1071,11 +1071,11 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'beolink': dict({
|
||||
'listeners': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'peers': dict({
|
||||
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
|
||||
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
|
||||
}),
|
||||
'self': dict({
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.bang_olufsen.const import (
|
||||
DEVICE_BUTTON_EVENTS,
|
||||
DEVICE_BUTTONS,
|
||||
EVENT_TRANSLATION_MAP,
|
||||
BangOlufsenButtons,
|
||||
)
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
@@ -23,13 +22,13 @@ from .const import TEST_BUTTON_EVENT_ENTITY_ID
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_button_event_creation_balance(
|
||||
async def test_button_event_creation(
|
||||
hass: HomeAssistant,
|
||||
integration: None,
|
||||
entity_registry: EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test button event entities are created when using a Balance (Most devices support all buttons like the Balance)."""
|
||||
"""Test button event entities are created."""
|
||||
|
||||
# Add Button Event entity ids
|
||||
entity_ids = [
|
||||
@@ -73,43 +72,6 @@ async def test_button_event_creation_beoconnect_core(
|
||||
assert entity_ids_available == snapshot
|
||||
|
||||
|
||||
async def test_button_event_creation_beosound_premiere(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry_premiere: MockConfigEntry,
|
||||
mock_mozart_client: AsyncMock,
|
||||
entity_registry: EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test Bluetooth button event entity is not created when using a Beosound Premiere."""
|
||||
|
||||
# Load entry
|
||||
mock_config_entry_premiere.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry_premiere.entry_id)
|
||||
await mock_websocket_connection(hass, mock_mozart_client)
|
||||
|
||||
# Add Button Event entity ids
|
||||
premiere_buttons = DEVICE_BUTTONS.copy()
|
||||
premiere_buttons.remove(BangOlufsenButtons.BLUETOOTH.value)
|
||||
|
||||
entity_ids = [
|
||||
f"event.beosound_premiere_33333333_{underscore(button_type)}".replace(
|
||||
"preset", "favorite_"
|
||||
)
|
||||
for button_type in premiere_buttons
|
||||
]
|
||||
|
||||
# Check that the entities are available
|
||||
for entity_id in entity_ids:
|
||||
assert entity_registry.async_get(entity_id)
|
||||
|
||||
# Check number of entities
|
||||
# The media_player entity and all of the button event entities (except Bluetooth) should be the only available
|
||||
entity_ids_available = list(entity_registry.entities.keys())
|
||||
assert len(entity_ids_available) == 1 + len(entity_ids)
|
||||
|
||||
assert entity_ids_available == snapshot
|
||||
|
||||
|
||||
async def test_button(
|
||||
hass: HomeAssistant,
|
||||
integration: None,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Common fixtures for the Duck DNS tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.duckdns.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_SUBDOMAIN = "homeassistant"
|
||||
TEST_TOKEN = "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.duckdns.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock Duck DNS configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=f"{TEST_SUBDOMAIN}.duckdns.org",
|
||||
data={
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
entry_id="12345",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_update_duckdns() -> Generator[AsyncMock]:
|
||||
"""Mock _update_duckdns."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.duckdns.config_flow._update_duckdns",
|
||||
return_value=True,
|
||||
) as mock:
|
||||
yield mock
|
||||
@@ -1,255 +0,0 @@
|
||||
"""Test the Duck DNS config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.duckdns.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import TEST_SUBDOMAIN, TEST_TOKEN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_update_duckdns")
|
||||
async def test_form(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: "123e4567-e89b-12d3-a456-426614174000",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
|
||||
assert result["data"] == {
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_update_duckdns")
|
||||
async def test_form_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we abort if already configured."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: "123e4567-e89b-12d3-a456-426614174000",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_unknown_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_update_duckdns: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we handle unknown exception."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_update_duckdns.side_effect = ValueError
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
mock_update_duckdns.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
|
||||
assert result["data"] == {
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_update_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_update_duckdns: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_update_duckdns.return_value = False
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "update_failed"}
|
||||
|
||||
mock_update_duckdns.return_value = True
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
|
||||
assert result["data"] == {
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_update_duckdns")
|
||||
async def test_import(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test import flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
|
||||
assert result["data"] == {
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=HOMEASSISTANT_DOMAIN,
|
||||
issue_id=f"deprecated_yaml_{DOMAIN}",
|
||||
)
|
||||
|
||||
|
||||
async def test_import_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_update_duckdns: AsyncMock,
|
||||
) -> None:
|
||||
"""Test import flow failed."""
|
||||
mock_update_duckdns.return_value = False
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "update_failed"
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id="deprecated_yaml_import_issue_error",
|
||||
)
|
||||
|
||||
|
||||
async def test_import_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_update_duckdns: AsyncMock,
|
||||
) -> None:
|
||||
"""Test import flow failed unknown."""
|
||||
mock_update_duckdns.side_effect = ValueError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_DOMAIN: TEST_SUBDOMAIN,
|
||||
CONF_ACCESS_TOKEN: TEST_TOKEN,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id="deprecated_yaml_import_issue_error",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_update_duckdns")
|
||||
async def test_init_import_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test yaml triggers import flow."""
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{"duckdns": {CONF_DOMAIN: TEST_SUBDOMAIN, CONF_ACCESS_TOKEN: TEST_TOKEN}},
|
||||
)
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
@@ -5,25 +5,19 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.duckdns import (
|
||||
ATTR_TXT,
|
||||
BACKOFF_INTERVALS,
|
||||
DOMAIN,
|
||||
INTERVAL,
|
||||
SERVICE_SET_TXT,
|
||||
UPDATE_URL,
|
||||
async_track_time_interval_backoff,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.components import duckdns
|
||||
from homeassistant.components.duckdns import async_track_time_interval_backoff
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .conftest import TEST_SUBDOMAIN, TEST_TOKEN
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
DOMAIN = "bla"
|
||||
TOKEN = "abcdefgh"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
INTERVAL = duckdns.INTERVAL
|
||||
|
||||
|
||||
async def async_set_txt(hass: HomeAssistant, txt: str | None) -> None:
|
||||
@@ -32,40 +26,37 @@ async def async_set_txt(hass: HomeAssistant, txt: str | None) -> None:
|
||||
This is a legacy helper method. Do not use it for new tests.
|
||||
"""
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_SET_TXT, {ATTR_TXT: txt}, blocking=True
|
||||
duckdns.DOMAIN, duckdns.SERVICE_SET_TXT, {duckdns.ATTR_TXT: txt}, blocking=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_duckdns(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
config_entry: MockConfigEntry,
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Fixture that sets up DuckDNS."""
|
||||
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
|
||||
text="OK",
|
||||
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="OK"
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
await async_setup_component(
|
||||
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_duckdns")
|
||||
async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
|
||||
"""Test setup works if update passes."""
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
|
||||
text="OK",
|
||||
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="OK"
|
||||
)
|
||||
|
||||
result = await async_setup_component(
|
||||
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
|
||||
@@ -74,47 +65,50 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -
|
||||
|
||||
|
||||
async def test_setup_backoff(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
config_entry: MockConfigEntry,
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test setup fails if first update fails."""
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
|
||||
text="KO",
|
||||
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="KO"
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
result = await async_setup_component(
|
||||
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
|
||||
)
|
||||
assert result
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
# Copy of the DuckDNS intervals from duckdns/__init__.py
|
||||
intervals = (
|
||||
INTERVAL,
|
||||
timedelta(minutes=1),
|
||||
timedelta(minutes=5),
|
||||
timedelta(minutes=15),
|
||||
timedelta(minutes=30),
|
||||
)
|
||||
tme = utcnow()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_LOGGER.debug("Backoff")
|
||||
for idx in range(1, len(BACKOFF_INTERVALS)):
|
||||
tme += BACKOFF_INTERVALS[idx]
|
||||
for idx in range(1, len(intervals)):
|
||||
tme += intervals[idx]
|
||||
async_fire_time_changed(hass, tme)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == idx + 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_duckdns")
|
||||
async def test_service_set_txt(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_duckdns
|
||||
) -> None:
|
||||
"""Test set txt service call."""
|
||||
# Empty the fixture mock requests
|
||||
aioclient_mock.clear_requests()
|
||||
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN, "txt": "some-txt"},
|
||||
duckdns.UPDATE_URL,
|
||||
params={"domains": DOMAIN, "token": TOKEN, "txt": "some-txt"},
|
||||
text="OK",
|
||||
)
|
||||
|
||||
@@ -123,22 +117,16 @@ async def test_service_set_txt(
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_duckdns")
|
||||
async def test_service_clear_txt(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_duckdns
|
||||
) -> None:
|
||||
"""Test clear txt service call."""
|
||||
# Empty the fixture mock requests
|
||||
aioclient_mock.clear_requests()
|
||||
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={
|
||||
"domains": TEST_SUBDOMAIN,
|
||||
"token": TEST_TOKEN,
|
||||
"txt": "",
|
||||
"clear": "true",
|
||||
},
|
||||
duckdns.UPDATE_URL,
|
||||
params={"domains": DOMAIN, "token": TOKEN, "txt": "", "clear": "true"},
|
||||
text="OK",
|
||||
)
|
||||
|
||||
@@ -206,28 +194,3 @@ async def test_async_track_time_interval_backoff(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert call_count == _idx
|
||||
|
||||
|
||||
async def test_load_unload(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test loading and unloading of the config entry."""
|
||||
|
||||
aioclient_mock.get(
|
||||
UPDATE_URL,
|
||||
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
|
||||
text="OK",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
@@ -1,923 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor[sensor.home_apparent_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_apparent_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Apparent temperature',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'apparent_temperature',
|
||||
'unique_id': 'home-subentry-id_feelsliketemperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_apparent_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Home Apparent temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_apparent_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_atmospheric_pressure-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_atmospheric_pressure',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ATMOSPHERIC_PRESSURE: 'atmospheric_pressure'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Atmospheric pressure',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_airpressure',
|
||||
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_atmospheric_pressure-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'friendly_name': 'Home Atmospheric pressure',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_atmospheric_pressure',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1019.16',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_cloud_coverage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_cloud_coverage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cloud coverage',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cloud_coverage',
|
||||
'unique_id': 'home-subentry-id_cloudcover',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_cloud_coverage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Cloud coverage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_cloud_coverage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_dew_point-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_dew_point',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Dew point',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'dew_point',
|
||||
'unique_id': 'home-subentry-id_dewpoint',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_dew_point-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Home Dew point',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_dew_point',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_heat_index_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_heat_index_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Heat index temperature',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'heat_index',
|
||||
'unique_id': 'home-subentry-id_heatindex',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_heat_index_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Home Heat index temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_heat_index_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13.7',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_relativehumidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Home Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '42',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_precipitation_intensity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_precipitation_intensity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PRECIPITATION_INTENSITY: 'precipitation_intensity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Precipitation intensity',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_precipitation_qpf',
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_precipitation_intensity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'precipitation_intensity',
|
||||
'friendly_name': 'Home Precipitation intensity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_precipitation_intensity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_precipitation_probability-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_precipitation_probability',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Precipitation probability',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'precipitation_probability',
|
||||
'unique_id': 'home-subentry-id_precipitation_probability',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_precipitation_probability-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Precipitation probability',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_precipitation_probability',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Home Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13.7',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_thunderstorm_probability-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_thunderstorm_probability',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Thunderstorm probability',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'thunderstorm_probability',
|
||||
'unique_id': 'home-subentry-id_thunderstormprobability',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_thunderstorm_probability-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Thunderstorm probability',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_thunderstorm_probability',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_uv_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_uv_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'UV index',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'uv_index',
|
||||
'unique_id': 'home-subentry-id_uvindex',
|
||||
'unit_of_measurement': 'UV index',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_uv_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home UV index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'UV index',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_uv_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_visibility-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_visibility',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Visibility',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'visibility',
|
||||
'unique_id': 'home-subentry-id_visibility',
|
||||
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_visibility-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'Home Visibility',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_visibility',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '16.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_weather_condition-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_weather_condition',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Weather condition',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'weather_condition',
|
||||
'unique_id': 'home-subentry-id_weathercondition',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_weather_condition-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Home Weather condition',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_weather_condition',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Sunny',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_chill_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_wind_chill_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind chill temperature',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_chill',
|
||||
'unique_id': 'home-subentry-id_windchill',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_chill_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Home Wind chill temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_wind_chill_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_direction-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_wind_direction',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_DIRECTION: 'wind_direction'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind direction',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_wind_direction',
|
||||
'unit_of_measurement': '°',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_direction-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'wind_direction',
|
||||
'friendly_name': 'Home Wind direction',
|
||||
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
'unit_of_measurement': '°',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_wind_direction',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '335',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_gust_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_wind_gust_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind gust speed',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_gust_speed',
|
||||
'unique_id': 'home-subentry-id_wind_gust',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_gust_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'Home Wind gust speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_wind_gust_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '18.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.home_wind_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind speed',
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id_wind_speed',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.home_wind_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'Home Wind speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_wind_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '8.0',
|
||||
})
|
||||
# ---
|
||||
@@ -1,168 +0,0 @@
|
||||
"""Test sensor of Google Weather integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test states of the sensor."""
|
||||
with patch("homeassistant.components.google_weather._PLATFORMS", [Platform.SENSOR]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_availability(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Ensure that we mark the entities unavailable correctly when service is offline."""
|
||||
entity_id = "sensor.home_temperature"
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "13.7"
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = (
|
||||
GoogleWeatherApiError()
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "13.7"
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_manual_update_entity(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test manual update entity via service homeassistant/update_entity."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_once_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{ATTR_ENTITY_ID: ["sensor.home_temperature"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_imperial_units(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test states of the sensor with imperial units."""
|
||||
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get("sensor.home_temperature")
|
||||
assert state
|
||||
assert state.state == "56.66"
|
||||
assert (
|
||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.FAHRENHEIT
|
||||
)
|
||||
|
||||
state = hass.states.get("sensor.home_wind_speed")
|
||||
assert state
|
||||
assert float(state.state) == pytest.approx(4.97097)
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfSpeed.MILES_PER_HOUR
|
||||
|
||||
state = hass.states.get("sensor.home_visibility")
|
||||
assert state
|
||||
assert float(state.state) == pytest.approx(9.94194)
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES
|
||||
|
||||
state = hass.states.get("sensor.home_atmospheric_pressure")
|
||||
assert state
|
||||
assert float(state.state) == pytest.approx(30.09578)
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.INHG
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_state_update(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Ensure the sensor state changes after updating the data."""
|
||||
entity_id = "sensor.home_temperature"
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "13.7"
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.return_value.temperature.degrees = 15.0
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "15.0"
|
||||
@@ -3,18 +3,9 @@
|
||||
from dataclasses import replace
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from pathlib import PurePath
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
from aiohasupervisor.models.mounts import (
|
||||
CIFSMountResponse,
|
||||
MountsInfo,
|
||||
MountState,
|
||||
MountType,
|
||||
MountUsage,
|
||||
NFSMountResponse,
|
||||
)
|
||||
from aiohasupervisor.models.mounts import CIFSMountResponse, MountsInfo, MountState
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio import DOMAIN
|
||||
@@ -27,7 +18,6 @@ from .common import MOCK_REPOSITORIES, MOCK_STORE_ADDONS
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
|
||||
|
||||
@@ -240,16 +230,16 @@ async def test_mount_binary_sensor(
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
# Add a mount.
|
||||
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
|
||||
mock_mounts = [
|
||||
CIFSMountResponse(
|
||||
share="files",
|
||||
server="1.2.3.4",
|
||||
name="NAS",
|
||||
type=MountType.CIFS,
|
||||
usage=MountUsage.SHARE,
|
||||
type="cifs",
|
||||
usage="share",
|
||||
read_only=False,
|
||||
state=MountState.ACTIVE,
|
||||
user_path=PurePath("/share/nas"),
|
||||
user_path="/share/nas",
|
||||
)
|
||||
]
|
||||
supervisor_client.mounts.info = AsyncMock(
|
||||
@@ -292,115 +282,3 @@ async def test_mount_binary_sensor(
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1000))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert hass.states.get(entity_id) is not None
|
||||
|
||||
|
||||
async def test_mount_refresh_after_issue(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test hassio mount state is refreshed after an issue was send by the supervisor."""
|
||||
# Add a mount.
|
||||
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
|
||||
CIFSMountResponse(
|
||||
share="files",
|
||||
server="1.2.3.4",
|
||||
name="NAS",
|
||||
type=MountType.CIFS,
|
||||
usage=MountUsage.SHARE,
|
||||
read_only=False,
|
||||
state=MountState.ACTIVE,
|
||||
user_path=PurePath("/share/nas"),
|
||||
)
|
||||
]
|
||||
supervisor_client.mounts.info = AsyncMock(
|
||||
return_value=MountsInfo(default_backup_mount=None, mounts=mock_mounts)
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
result = await async_setup_component(
|
||||
hass,
|
||||
"hassio",
|
||||
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
|
||||
)
|
||||
assert result
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Enable the entity.
|
||||
entity_id = "binary_sensor.nas_connected"
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test new entity.
|
||||
entity = hass.states.get(entity_id)
|
||||
assert entity is not None
|
||||
assert entity.state == "on"
|
||||
|
||||
# Change mount state to failed, issue a repair, and verify entity's state.
|
||||
mock_mounts[0] = replace(mock_mounts[0], state=MountState.FAILED)
|
||||
client = await hass_ws_client(hass)
|
||||
issue_uuid = uuid4().hex
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "issue_changed",
|
||||
"data": {
|
||||
"uuid": issue_uuid,
|
||||
"type": "mount_failed",
|
||||
"context": "mount",
|
||||
"reference": "nas",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": uuid4().hex,
|
||||
"type": "execute_reload",
|
||||
"context": "mount",
|
||||
"reference": "nas",
|
||||
},
|
||||
{
|
||||
"uuid": uuid4().hex,
|
||||
"type": "execute_remove",
|
||||
"context": "mount",
|
||||
"reference": "nas",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
entity = hass.states.get(entity_id)
|
||||
assert entity is not None
|
||||
assert entity.state == "off"
|
||||
|
||||
# Change mount state to active, issue a repair, and verify entity's state.
|
||||
mock_mounts[0] = replace(mock_mounts[0], state=MountState.ACTIVE)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "issue_removed",
|
||||
"data": {
|
||||
"uuid": issue_uuid,
|
||||
"type": "mount_failed",
|
||||
"context": "mount",
|
||||
"reference": "nas",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
entity = hass.states.get(entity_id)
|
||||
assert entity is not None
|
||||
assert entity.state == "on"
|
||||
|
||||
@@ -108,24 +108,6 @@
|
||||
"enumber": "HCS000000/06",
|
||||
"haId": "123456789012345678"
|
||||
},
|
||||
{
|
||||
"name": "AirConditioner",
|
||||
"brand": "BOSCH",
|
||||
"vib": "HCS000006",
|
||||
"connected": true,
|
||||
"type": "AirConditioner",
|
||||
"enumber": "HCS000000/07",
|
||||
"haId": "8765432109876543210"
|
||||
},
|
||||
{
|
||||
"name": "Microwave",
|
||||
"brand": "BOSCH",
|
||||
"vib": "HCS000006",
|
||||
"connected": true,
|
||||
"type": "Microwave",
|
||||
"enumber": "HCS000000/08",
|
||||
"haId": "541513213246313789"
|
||||
},
|
||||
{
|
||||
"name": "DNE",
|
||||
"brand": "BOSCH",
|
||||
|
||||
@@ -205,47 +205,5 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"AirConditioner": {
|
||||
"data": {
|
||||
"programs": [
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.ActiveClean",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.Auto",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.Cool",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.Dry",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.Fan",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "HeatingVentilationAirConditioning.AirConditioner.Program.Heat",
|
||||
"constraints": {
|
||||
"execution": "startonly"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,72 +31,6 @@
|
||||
'type': 'CookProcessor',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'541513213246313789': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/08',
|
||||
'ha_id': '541513213246313789',
|
||||
'name': 'Microwave',
|
||||
'programs': list([
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
}),
|
||||
'type': 'Microwave',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'8765432109876543210': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/07',
|
||||
'ha_id': '8765432109876543210',
|
||||
'name': 'AirConditioner',
|
||||
'programs': list([
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.ActiveClean',
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.Auto',
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.Cool',
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.Dry',
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.Fan',
|
||||
'HeatingVentilationAirConditioning.AirConditioner.Program.Heat',
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
}),
|
||||
'type': 'AirConditioner',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'BOSCH-000000000-000000000000': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
|
||||
@@ -16,7 +16,6 @@ from ha_silabs_firmware_client import (
|
||||
import pytest
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.const import Z2M_EMBER_DOCS_URL
|
||||
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
|
||||
STEP_PICK_FIRMWARE_THREAD,
|
||||
STEP_PICK_FIRMWARE_ZIGBEE,
|
||||
@@ -578,26 +577,12 @@ async def test_config_flow_zigbee_custom_other(hass: HomeAssistant) -> None:
|
||||
assert pick_result["progress_action"] == "install_firmware"
|
||||
assert pick_result["step_id"] == "install_zigbee_firmware"
|
||||
|
||||
show_z2m_result = await consume_progress_flow(
|
||||
create_result = await consume_progress_flow(
|
||||
hass,
|
||||
flow_id=pick_result["flow_id"],
|
||||
valid_step_ids=("install_zigbee_firmware",),
|
||||
)
|
||||
|
||||
# After firmware installation, Z2M docs link is shown
|
||||
assert show_z2m_result["type"] is FlowResultType.FORM
|
||||
assert show_z2m_result["step_id"] == "show_z2m_docs_url"
|
||||
assert (
|
||||
show_z2m_result["description_placeholders"]["z2m_docs_url"]
|
||||
== Z2M_EMBER_DOCS_URL
|
||||
)
|
||||
|
||||
# Submit the form to complete the flow
|
||||
create_result = await hass.config_entries.flow.async_configure(
|
||||
show_z2m_result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
|
||||
assert create_result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
config_entry = create_result["result"]
|
||||
|
||||
@@ -63,25 +63,12 @@ async def async_manipulate_test_data(
|
||||
new_value: Any,
|
||||
channel: int = 1,
|
||||
fire_device: HomeMaticIPObject | None = None,
|
||||
channel_real_index: int | None = None,
|
||||
):
|
||||
"""Set new value on hmip device."""
|
||||
if channel == 1:
|
||||
setattr(hmip_device, attribute, new_value)
|
||||
|
||||
channels = getattr(hmip_device, "functionalChannels", None)
|
||||
if channels:
|
||||
if channel_real_index is not None:
|
||||
functional_channel = next(
|
||||
(ch for ch in channels if ch.index == channel_real_index),
|
||||
None,
|
||||
)
|
||||
assert functional_channel is not None, (
|
||||
f"No functional channel with index {channel_real_index} found in hmip_device.functionalChannels"
|
||||
)
|
||||
else:
|
||||
functional_channel = channels[channel]
|
||||
|
||||
if hasattr(hmip_device, "functionalChannels"):
|
||||
functional_channel = hmip_device.functionalChannels[channel]
|
||||
setattr(functional_channel, attribute, new_value)
|
||||
|
||||
fire_target = hmip_device if fire_device is None else fire_device
|
||||
|
||||
@@ -565,8 +565,8 @@ async def test_hmip_multi_contact_interface(
|
||||
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
|
||||
) -> None:
|
||||
"""Test HomematicipMultiContactInterface."""
|
||||
entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel10"
|
||||
entity_name = "Wired Eingangsmodul – 32-fach Channel10"
|
||||
entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel5"
|
||||
entity_name = "Wired Eingangsmodul – 32-fach Channel5"
|
||||
device_model = "HmIPW-DRI32"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Wired Eingangsmodul – 32-fach", "Licht Flur"]
|
||||
@@ -578,25 +578,15 @@ async def test_hmip_multi_contact_interface(
|
||||
|
||||
assert ha_state.state == STATE_OFF
|
||||
await async_manipulate_test_data(
|
||||
hass, hmip_device, "windowState", WindowState.OPEN, channel_real_index=10
|
||||
hass, hmip_device, "windowState", WindowState.OPEN, channel=5
|
||||
)
|
||||
ha_state = hass.states.get(entity_id)
|
||||
assert ha_state.state == STATE_ON
|
||||
|
||||
await async_manipulate_test_data(
|
||||
hass, hmip_device, "windowState", None, channel_real_index=10
|
||||
)
|
||||
await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5)
|
||||
ha_state = hass.states.get(entity_id)
|
||||
assert ha_state.state == STATE_UNKNOWN
|
||||
|
||||
# Test channel 32 of device
|
||||
entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel32"
|
||||
entity_name = "Wired Eingangsmodul – 32-fach Channel32"
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
hass, mock_hap, entity_id, entity_name, device_model
|
||||
)
|
||||
assert ha_state.state == STATE_OFF
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
hass,
|
||||
mock_hap,
|
||||
|
||||
@@ -374,7 +374,7 @@ async def test_storage_dashboards(
|
||||
assert response["success"]
|
||||
assert response["result"] == []
|
||||
|
||||
# Add a wrong dashboard (no hyphen)
|
||||
# Add a wrong dashboard
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
@@ -484,36 +484,16 @@ async def test_storage_dashboards(
|
||||
assert response["result"][0]["show_in_sidebar"] is False
|
||||
assert response["result"][0]["require_admin"] is False
|
||||
|
||||
# Add a wrong dashboard (missing title)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 14,
|
||||
"type": "lovelace/dashboards/create",
|
||||
"url_path": "path",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "invalid_format"
|
||||
|
||||
# Add dashboard with existing url path
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 15,
|
||||
"type": "lovelace/dashboards/create",
|
||||
"url_path": "created-url-path",
|
||||
"title": "Another title",
|
||||
}
|
||||
{"id": 14, "type": "lovelace/dashboards/create", "url_path": "created-url-path"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "home_assistant_error"
|
||||
assert response["error"]["translation_key"] == "url_already_exists"
|
||||
assert response["error"]["translation_placeholders"]["url"] == "created-url-path"
|
||||
|
||||
# Delete dashboards
|
||||
await client.send_json(
|
||||
{"id": 16, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id}
|
||||
{"id": 15, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.lutron_caseta import DOMAIN
|
||||
from homeassistant.components.lutron_caseta.const import (
|
||||
@@ -90,9 +90,7 @@ _LEAP_DEVICE_TYPES = {
|
||||
class MockBridge:
|
||||
"""Mock Lutron bridge that emulates configured connected status."""
|
||||
|
||||
def __init__(
|
||||
self, can_connect=True, timeout_on_connect=False, smart_away_state=""
|
||||
) -> None:
|
||||
def __init__(self, can_connect=True, timeout_on_connect=False) -> None:
|
||||
"""Initialize MockBridge instance with configured mock connectivity."""
|
||||
self.timeout_on_connect = timeout_on_connect
|
||||
self.can_connect = can_connect
|
||||
@@ -103,23 +101,6 @@ class MockBridge:
|
||||
self.devices = self.load_devices()
|
||||
self.buttons = self.load_buttons()
|
||||
self._subscribers: dict[str, list] = {}
|
||||
self.smart_away_state = smart_away_state
|
||||
self._smart_away_subscribers = []
|
||||
|
||||
self.activate_smart_away = AsyncMock(side_effect=self._activate)
|
||||
self.deactivate_smart_away = AsyncMock(side_effect=self._deactivate)
|
||||
|
||||
async def _activate(self):
|
||||
"""Activate smart away."""
|
||||
self.smart_away_state = "Enabled"
|
||||
for callback in self._smart_away_subscribers:
|
||||
callback()
|
||||
|
||||
async def _deactivate(self):
|
||||
"""Deactivate smart away."""
|
||||
self.smart_away_state = "Disabled"
|
||||
for callback in self._smart_away_subscribers:
|
||||
callback()
|
||||
|
||||
async def connect(self):
|
||||
"""Connect the mock bridge."""
|
||||
@@ -134,10 +115,6 @@ class MockBridge:
|
||||
self._subscribers[device_id] = []
|
||||
self._subscribers[device_id].append(callback_)
|
||||
|
||||
def add_smart_away_subscriber(self, callback_):
|
||||
"""Add a smart away subscriber."""
|
||||
self._smart_away_subscribers.append(callback_)
|
||||
|
||||
def add_button_subscriber(self, button_id: str, callback_):
|
||||
"""Mock a listener for button presses."""
|
||||
|
||||
@@ -377,7 +354,6 @@ async def async_setup_integration(
|
||||
can_connect: bool = True,
|
||||
timeout_during_connect: bool = False,
|
||||
timeout_during_configure: bool = False,
|
||||
smart_away_state: str = "",
|
||||
) -> MockConfigEntry:
|
||||
"""Set up a mock bridge."""
|
||||
if config_entry_id is None:
|
||||
@@ -394,9 +370,7 @@ async def async_setup_integration(
|
||||
if not timeout_during_connect:
|
||||
on_connect_callback()
|
||||
return mock_bridge(
|
||||
can_connect=can_connect,
|
||||
timeout_on_connect=timeout_during_configure,
|
||||
smart_away_state=smart_away_state,
|
||||
can_connect=can_connect, timeout_on_connect=timeout_during_configure
|
||||
)
|
||||
|
||||
with patch(
|
||||
|
||||
@@ -180,7 +180,6 @@ async def test_diagnostics(
|
||||
},
|
||||
"occupancy_groups": {},
|
||||
"scenes": {},
|
||||
"smart_away_state": "",
|
||||
},
|
||||
"entry": {
|
||||
"data": {"ca_certs": "", "certfile": "", "host": "1.1.1.1", "keyfile": ""},
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
"""Tests for the Lutron Caseta integration."""
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -24,88 +16,3 @@ async def test_switch_unique_id(
|
||||
|
||||
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
|
||||
assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803"
|
||||
|
||||
|
||||
async def test_smart_away_switch_setup(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test smart away switch is created when bridge supports it."""
|
||||
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
|
||||
|
||||
smart_away_entity_id = "switch.hallway_smart_away"
|
||||
|
||||
# Verify entity is registered
|
||||
entity_entry = entity_registry.async_get(smart_away_entity_id)
|
||||
assert entity_entry is not None
|
||||
assert entity_entry.unique_id == "000004d2_smart_away"
|
||||
|
||||
# Verify initial state is off
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_smart_away_switch_not_created_when_not_supported(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test smart away switch is not created when bridge doesn't support it."""
|
||||
|
||||
await async_setup_integration(hass, MockBridge)
|
||||
|
||||
smart_away_entity_id = "switch.hallway_smart_away"
|
||||
|
||||
# Verify entity is not registered
|
||||
entity_entry = entity_registry.async_get(smart_away_entity_id)
|
||||
assert entity_entry is None
|
||||
|
||||
# Verify state doesn't exist
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_smart_away_turn_on(hass: HomeAssistant) -> None:
|
||||
"""Test turning on smart away."""
|
||||
|
||||
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
|
||||
|
||||
smart_away_entity_id = "switch.hallway_smart_away"
|
||||
|
||||
# Verify initial state is off
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Turn on smart away
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: smart_away_entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Verify state is on
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_smart_away_turn_off(hass: HomeAssistant) -> None:
|
||||
"""Test turning off smart away."""
|
||||
|
||||
await async_setup_integration(hass, MockBridge, smart_away_state="Enabled")
|
||||
|
||||
smart_away_entity_id = "switch.hallway_smart_away"
|
||||
|
||||
# Verify initial state is off
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# Turn on smart away
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: smart_away_entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Verify state is on
|
||||
state = hass.states.get(smart_away_entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
@@ -282,100 +282,6 @@ async def test_subscribe_topic(
|
||||
unsub()
|
||||
|
||||
|
||||
async def test_status_subscription_done(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
recorded_calls: list[ReceiveMessage],
|
||||
record_calls: MessageCallbackType,
|
||||
) -> None:
|
||||
"""Test the on subscription status."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
on_status = asyncio.Event()
|
||||
on_status_calls: list[bool] = []
|
||||
|
||||
def _on_subscribe_status() -> None:
|
||||
on_status.set()
|
||||
on_status_calls.append(True)
|
||||
|
||||
subscribe_callback = await mqtt.async_subscribe(
|
||||
hass, "test-topic", record_calls, qos=0
|
||||
)
|
||||
handler = mqtt.async_on_subscribe_done(
|
||||
hass, "test-topic", 0, on_subscribe_status=_on_subscribe_status
|
||||
)
|
||||
await on_status.wait()
|
||||
assert ("test-topic", 0) in help_all_subscribe_calls(mqtt_client_mock)
|
||||
|
||||
await mqtt.async_publish(hass, "test-topic", "beer ready", 0)
|
||||
handler()
|
||||
assert len(recorded_calls) == 1
|
||||
assert recorded_calls[0].topic == "test-topic"
|
||||
assert recorded_calls[0].payload == "beer ready"
|
||||
assert recorded_calls[0].qos == 0
|
||||
|
||||
# Test as we have an existing subscription, test we get a callback
|
||||
recorded_calls.clear()
|
||||
on_status.clear()
|
||||
handler = mqtt.async_on_subscribe_done(
|
||||
hass, "test-topic", 0, on_subscribe_status=_on_subscribe_status
|
||||
)
|
||||
assert len(on_status_calls) == 1
|
||||
await on_status.wait()
|
||||
assert len(on_status_calls) == 2
|
||||
|
||||
# cleanup
|
||||
handler()
|
||||
subscribe_callback()
|
||||
|
||||
|
||||
async def test_subscribe_topic_with_subscribe_done(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
recorded_calls: list[ReceiveMessage],
|
||||
record_calls: MessageCallbackType,
|
||||
) -> None:
|
||||
"""Test the subscription of a topic."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
on_status = asyncio.Event()
|
||||
|
||||
def _on_subscribe() -> None:
|
||||
hass.async_create_task(mqtt.async_publish(hass, "test-topic", "beer ready", 0))
|
||||
on_status.set()
|
||||
|
||||
# Start a first subscription
|
||||
unsub1 = await mqtt.async_subscribe(
|
||||
hass, "test-topic", record_calls, on_subscribe=_on_subscribe
|
||||
)
|
||||
await on_status.wait()
|
||||
await hass.async_block_till_done()
|
||||
assert len(recorded_calls) == 1
|
||||
assert recorded_calls[0].topic == "test-topic"
|
||||
assert recorded_calls[0].payload == "beer ready"
|
||||
assert recorded_calls[0].qos == 0
|
||||
recorded_calls.clear()
|
||||
|
||||
# Start a second subscription to the same topic
|
||||
on_status.clear()
|
||||
unsub2 = await mqtt.async_subscribe(
|
||||
hass, "test-topic", record_calls, on_subscribe=_on_subscribe
|
||||
)
|
||||
await on_status.wait()
|
||||
await hass.async_block_till_done()
|
||||
assert len(recorded_calls) == 2
|
||||
assert recorded_calls[0].topic == "test-topic"
|
||||
assert recorded_calls[0].payload == "beer ready"
|
||||
assert recorded_calls[0].qos == 0
|
||||
assert recorded_calls[1].topic == "test-topic"
|
||||
assert recorded_calls[1].payload == "beer ready"
|
||||
assert recorded_calls[1].qos == 0
|
||||
|
||||
unsub1()
|
||||
unsub2()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mqtt_mock_entry")
|
||||
async def test_subscribe_topic_not_initialize(
|
||||
hass: HomeAssistant, record_calls: MessageCallbackType
|
||||
@@ -386,16 +292,6 @@ async def test_subscribe_topic_not_initialize(
|
||||
):
|
||||
await mqtt.async_subscribe(hass, "test-topic", record_calls)
|
||||
|
||||
def _on_subscribe_callback() -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match=r".*make sure MQTT is set up correctly"
|
||||
):
|
||||
await mqtt.async_subscribe(
|
||||
hass, "test-topic", record_calls, on_subscribe=_on_subscribe_callback
|
||||
)
|
||||
|
||||
|
||||
async def test_subscribe_mqtt_config_entry_disabled(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType
|
||||
|
||||
@@ -68,23 +68,6 @@ def mock_config_entry_with_assist(
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry_with_assist_invalid_api(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> MockConfigEntry:
|
||||
"""Mock a config entry with assist."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={
|
||||
**subentry.data,
|
||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST, "invalid_api"],
|
||||
},
|
||||
)
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfigEntry):
|
||||
"""Initialize integration."""
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import ollama
|
||||
from homeassistant.const import CONF_LLM_HASS_API, CONF_NAME
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@@ -463,27 +463,6 @@ async def test_subentry_reconfigure_with_download(
|
||||
}
|
||||
|
||||
|
||||
async def test_filter_invalid_llms(
|
||||
hass: HomeAssistant,
|
||||
mock_init_component,
|
||||
mock_config_entry_with_assist_invalid_api: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfiguring subentry when one of the configured LLM APIs has been removed."""
|
||||
subentry = next(iter(mock_config_entry_with_assist_invalid_api.subentries.values()))
|
||||
|
||||
assert len(subentry.data.get(CONF_LLM_HASS_API)) == 2
|
||||
assert "invalid_api" in subentry.data.get(CONF_LLM_HASS_API)
|
||||
assert "assist" in subentry.data.get(CONF_LLM_HASS_API)
|
||||
|
||||
valid_apis = ollama.config_flow.filter_invalid_llm_apis(
|
||||
hass, subentry.data[CONF_LLM_HASS_API]
|
||||
)
|
||||
|
||||
assert len(valid_apis) == 1
|
||||
assert "invalid_api" not in valid_apis
|
||||
assert "assist" in valid_apis
|
||||
|
||||
|
||||
async def test_creating_ai_task_subentry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -576,7 +576,7 @@ def _mock_rpc_device(version: str | None = None):
|
||||
zigbee_enabled=False,
|
||||
zigbee_firmware=False,
|
||||
ip_address="10.10.10.10",
|
||||
wifi_setconfig=AsyncMock(return_value={"restart_required": True}),
|
||||
wifi_setconfig=AsyncMock(return_value={}),
|
||||
ble_setconfig=AsyncMock(return_value={"restart_required": False}),
|
||||
shutdown=AsyncMock(),
|
||||
)
|
||||
|
||||
@@ -101,11 +101,6 @@ BLE_MANUFACTURER_DATA_WITH_MAC_UNKNOWN_MODEL = {
|
||||
0x0BA9: bytes.fromhex("0105000b99990a70d6c297bacc")
|
||||
} # Flags (0x01, 0x05, 0x00), Model (0x0b, 0x99, 0x99) - unknown model ID, MAC (0x0a, 0x70, 0xd6, 0xc2, 0x97, 0xba, 0xcc)
|
||||
|
||||
BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST = {
|
||||
0x0BA9: bytes.fromhex("0105000b30100a00eeddccbbaa")
|
||||
} # Flags (0x01, 0x05, 0x00), Model (0x0b, 0x30, 0x10), MAC (0x0a, 0x00, 0xee, 0xdd, 0xcc, 0xbb, 0xaa)
|
||||
# Device WiFi MAC: 00eeddccbbaa (little-endian) -> AABBCCDDEE00 (reversed to big-endian)
|
||||
|
||||
BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
name="ShellyPlus2PM-C049EF8873E8",
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
@@ -126,26 +121,6 @@ BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
BLE_DISCOVERY_INFO_FOR_CLEAR_TEST = BluetoothServiceInfoBleak(
|
||||
name="ShellyPlus2PM-AABBCCDDEE00",
|
||||
address="AA:BB:CC:DD:EE:00",
|
||||
rssi=-60,
|
||||
manufacturer_data=BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST,
|
||||
service_uuids=[],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=generate_ble_device(
|
||||
address="AA:BB:CC:DD:EE:00",
|
||||
name="ShellyPlus2PM-AABBCCDDEE00",
|
||||
),
|
||||
advertisement=generate_advertisement_data(
|
||||
manufacturer_data=BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST,
|
||||
),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
BLE_DISCOVERY_INFO_NO_RPC = BluetoothServiceInfoBleak(
|
||||
name="ShellyPlus2PM-C049EF8873E8",
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
@@ -2102,87 +2077,6 @@ async def test_bluetooth_discovery(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_bluetooth_provisioning_clears_match_history(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_setup: AsyncMock,
|
||||
) -> None:
|
||||
"""Test bluetooth provisioning clears match history at discovery start and after successful provisioning."""
|
||||
# Inject BLE device so it's available in the bluetooth scanner
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO_FOR_CLEAR_TEST)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_clear_address_from_match_history",
|
||||
) as mock_clear:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=BLE_DISCOVERY_INFO_FOR_CLEAR_TEST,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
# Confirm
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_scan_wifi_networks",
|
||||
return_value=[{"ssid": "MyNetwork", "rssi": -50, "auth": 2}],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {}
|
||||
)
|
||||
|
||||
# Select network
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_SSID: "MyNetwork"},
|
||||
)
|
||||
|
||||
# Reset mock to only count calls during provisioning
|
||||
mock_clear.reset_mock()
|
||||
|
||||
# Enter password and provision
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_provision_wifi",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_lookup_device_by_name",
|
||||
return_value=("1.1.1.1", 80),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.get_info",
|
||||
return_value=MOCK_DEVICE_INFO,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "my_password"},
|
||||
)
|
||||
|
||||
# Provisioning happens in background, shows progress
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Complete provisioning by configuring the progress step
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
# Provisioning should complete and create entry
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].unique_id == "AABBCCDDEE00"
|
||||
|
||||
# Verify match history was cleared once during provisioning
|
||||
# Only count calls with our test device's address to avoid interference from other tests
|
||||
our_device_calls = [
|
||||
call
|
||||
for call in mock_clear.call_args_list
|
||||
if len(call.args) > 1
|
||||
and call.args[1] == BLE_DISCOVERY_INFO_FOR_CLEAR_TEST.address
|
||||
]
|
||||
assert our_device_calls
|
||||
mock_clear.assert_called_with(hass, BLE_DISCOVERY_INFO_FOR_CLEAR_TEST.address)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_bluetooth_discovery_no_rpc_over_ble(
|
||||
hass: HomeAssistant,
|
||||
@@ -2198,88 +2092,6 @@ async def test_bluetooth_discovery_no_rpc_over_ble(
|
||||
assert result["reason"] == "invalid_discovery_info"
|
||||
|
||||
|
||||
async def test_bluetooth_factory_reset_rediscovery(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_setup: AsyncMock,
|
||||
) -> None:
|
||||
"""Test device can be rediscovered after factory reset when RPC-over-BLE is re-enabled."""
|
||||
# First discovery: device is already provisioned (no RPC-over-BLE)
|
||||
# Inject the device without RPC so it's in the bluetooth scanner
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO_NO_RPC)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=BLE_DISCOVERY_INFO_NO_RPC,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
)
|
||||
|
||||
# Should abort because RPC-over-BLE is not enabled
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_discovery_info"
|
||||
|
||||
# Simulate factory reset: device now advertises with RPC-over-BLE enabled
|
||||
# Inject the updated advertisement
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
|
||||
|
||||
# Second discovery: device after factory reset (RPC-over-BLE now enabled)
|
||||
# Wait for automatic discovery to happen
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Find the flow that was automatically created
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
result = flows[0]
|
||||
|
||||
# Should successfully start config flow since match history was cleared
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert (
|
||||
result["context"]["title_placeholders"]["name"] == "ShellyPlus2PM-C049EF8873E8"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_scan_wifi_networks",
|
||||
return_value=[{"ssid": "MyNetwork", "rssi": -50, "auth": 2}],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
# Select network
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_SSID: "MyNetwork"},
|
||||
)
|
||||
|
||||
# Enter password and provision
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_provision_wifi",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_lookup_device_by_name",
|
||||
return_value=("1.1.1.1", 80),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.get_info",
|
||||
return_value=MOCK_DEVICE_INFO,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "my_password"},
|
||||
)
|
||||
|
||||
# Provisioning happens in background
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Complete provisioning
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
# Provisioning should complete and create entry
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].unique_id == "C049EF8873E8"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_bluetooth_discovery_invalid_name(
|
||||
hass: HomeAssistant,
|
||||
@@ -2372,41 +2184,6 @@ async def test_bluetooth_discovery_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_already_configured_clears_match_history(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test bluetooth discovery clears match history when device already configured."""
|
||||
# Inject BLE device so it's available in the bluetooth scanner
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="C049EF8873E8", # MAC from device name - uppercase no colons
|
||||
data={
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_MODEL: MODEL_PLUS_2PM,
|
||||
CONF_SLEEP_PERIOD: 0,
|
||||
CONF_GEN: 2,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_clear_address_from_match_history"
|
||||
) as mock_clear:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=BLE_DISCOVERY_INFO,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
# Verify match history was cleared to allow rediscovery if factory reset
|
||||
mock_clear.assert_called_once_with(hass, BLE_DISCOVERY_INFO.address)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_bluetooth_discovery_no_ble_device(
|
||||
hass: HomeAssistant,
|
||||
@@ -3369,163 +3146,6 @@ async def test_bluetooth_provision_timeout_active_lookup_fails(
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
|
||||
async def test_bluetooth_provision_timeout_ble_fallback_succeeds(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_setup: AsyncMock,
|
||||
) -> None:
|
||||
"""Test WiFi provisioning times out, active lookup fails, but BLE fallback succeeds."""
|
||||
# Inject BLE device
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=BLE_DISCOVERY_INFO,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
)
|
||||
|
||||
# Confirm and scan
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_scan_wifi_networks",
|
||||
return_value=[{"ssid": "MyNetwork", "rssi": -50, "auth": 2}],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
# Select network
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_SSID: "MyNetwork"},
|
||||
)
|
||||
|
||||
# Mock device for BLE status query
|
||||
mock_ble_status_device = AsyncMock()
|
||||
mock_ble_status_device.status = {"wifi": {"sta_ip": "192.168.1.100"}}
|
||||
|
||||
# Mock device for secure device feature
|
||||
mock_device = AsyncMock()
|
||||
mock_device.initialize = AsyncMock()
|
||||
mock_device.name = "Test name"
|
||||
mock_device.status = {"sys": {}}
|
||||
mock_device.xmod_info = {}
|
||||
mock_device.shelly = {"model": MODEL_PLUS_2PM}
|
||||
mock_device.wifi_setconfig = AsyncMock(return_value={})
|
||||
mock_device.ble_setconfig = AsyncMock(return_value={"restart_required": False})
|
||||
mock_device.shutdown = AsyncMock()
|
||||
|
||||
# Provision WiFi but no zeroconf discovery arrives, active lookup fails, BLE fallback succeeds
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.PROVISIONING_TIMEOUT",
|
||||
0.01, # Short timeout to trigger timeout path
|
||||
),
|
||||
patch("homeassistant.components.shelly.config_flow.async_provision_wifi"),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_lookup_device_by_name",
|
||||
return_value=None, # Active lookup fails
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.ble_rpc_device",
|
||||
) as mock_ble_rpc,
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.get_info",
|
||||
return_value=MOCK_DEVICE_INFO,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.RpcDevice.create",
|
||||
return_value=mock_device,
|
||||
),
|
||||
):
|
||||
# Configure BLE RPC mock to return device with IP
|
||||
mock_ble_rpc.return_value.__aenter__.return_value = mock_ble_status_device
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "my_password"},
|
||||
)
|
||||
|
||||
# Provisioning shows progress
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Timeout occurs, active lookup fails, but BLE fallback gets IP
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
# Should create entry successfully with IP from BLE
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Test name"
|
||||
assert result["data"][CONF_HOST] == "192.168.1.100"
|
||||
assert result["data"][CONF_PORT] == DEFAULT_HTTP_PORT
|
||||
|
||||
|
||||
async def test_bluetooth_provision_timeout_ble_fallback_fails(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test WiFi provisioning times out, active lookup fails, and BLE fallback also fails."""
|
||||
# Inject BLE device
|
||||
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=BLE_DISCOVERY_INFO,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
)
|
||||
|
||||
# Confirm and scan
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_scan_wifi_networks",
|
||||
return_value=[{"ssid": "MyNetwork", "rssi": -50, "auth": 2}],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
# Select network
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_SSID: "MyNetwork"},
|
||||
)
|
||||
|
||||
# Provision WiFi but no zeroconf discovery, active lookup fails, BLE fallback fails
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.PROVISIONING_TIMEOUT",
|
||||
0.01, # Short timeout to trigger timeout path
|
||||
),
|
||||
patch("homeassistant.components.shelly.config_flow.async_provision_wifi"),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_lookup_device_by_name",
|
||||
return_value=None, # Active lookup fails
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.async_get_ip_from_ble",
|
||||
return_value=None, # BLE fallback also fails
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "my_password"},
|
||||
)
|
||||
|
||||
# Provisioning shows progress
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Timeout occurs, both active lookup and BLE fallback fail
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
# Should show provision_failed form
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "provision_failed"
|
||||
|
||||
# User aborts after failure
|
||||
with patch(
|
||||
"homeassistant.components.shelly.config_flow.async_scan_wifi_networks",
|
||||
side_effect=RuntimeError("BLE device unavailable"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
|
||||
async def test_bluetooth_provision_secure_device_both_enabled(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.shelly.const import (
|
||||
CONF_BLE_SCANNER_MODE,
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID,
|
||||
DOMAIN,
|
||||
OPEN_WIFI_AP_ISSUE_ID,
|
||||
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID,
|
||||
BLEScannerMode,
|
||||
DeprecatedFirmwareInfo,
|
||||
@@ -255,207 +254,3 @@ async def test_deprecated_firmware_issue(
|
||||
# Assert the issue is no longer present
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test repair issues handling for open WiFi AP."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "is_open": True}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "create_entry"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False}
|
||||
assert mock_rpc_device.trigger_reboot.call_count == 1
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue_no_restart(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test repair issues handling for open WiFi AP when restart not required."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "is_open": True}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
mock_rpc_device.wifi_setconfig.return_value = {"restart_required": False}
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "create_entry"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False}
|
||||
assert mock_rpc_device.trigger_reboot.call_count == 0
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")]
|
||||
)
|
||||
async def test_open_wifi_ap_issue_exc(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test repair issues handling when wifi_setconfig ends with an exception."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "is_open": True}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
mock_rpc_device.wifi_setconfig.side_effect = exception
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "cannot_connect"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_no_open_wifi_ap_issue_with_password(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test no repair issue is created when WiFi AP has a password."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "is_open": False}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_no_open_wifi_ap_issue_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test no repair issue is created when WiFi AP is disabled."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": False, "is_open": True}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue_ignore(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test ignoring the open WiFi AP issue."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "is_open": True}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "ignore"})
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "issue_ignored"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 0
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id).dismissed_version
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user