mirror of
https://github.com/home-assistant/core.git
synced 2026-05-23 01:05:20 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36ee46ca8a | |||
| bfd36858f9 | |||
| 55d7892369 | |||
| 0539296683 | |||
| 94581d8ab6 | |||
| 7d6ec7fc58 | |||
| f49de3548e | |||
| 49ab42d3a2 | |||
| 383f6142f0 | |||
| 2f120cf604 | |||
| 37288849b3 | |||
| aa8659f507 | |||
| 40c0d79d1d | |||
| bef8632d78 | |||
| f00decfaa3 | |||
| 42e7add026 | |||
| 263aa3f16e | |||
| 03b364dcf0 | |||
| 3b1aaf39af | |||
| b82ba43fa4 | |||
| d81ef5593c | |||
| 5c5e50f024 | |||
| e796d9c467 | |||
| 342f23526f | |||
| 814ec697cf | |||
| 120f1446d4 | |||
| 170af75b7d | |||
| 5432d29489 | |||
| 8098f4f6bc | |||
| 6a70077687 | |||
| 5dbb0464ba | |||
| 1df165ea02 | |||
| 62542eb911 | |||
| a842cac34c | |||
| 2460f688e3 | |||
| a868ea443c | |||
| 1d8565483b |
@@ -15,6 +15,7 @@ Dockerfile.dev linguist-language=Dockerfile
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
|
||||
machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"service": "mdi:dialpad"
|
||||
},
|
||||
"alarm_toggle_chime": {
|
||||
"service": "mdi:abc"
|
||||
"service": "mdi:bell-ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Invalid authentication credentials: {error}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
|
||||
@@ -5,8 +5,12 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.components.hassio import HassioNotReadyError
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -49,6 +53,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
_DATA_SNAPSHOTS_URL: HassKey[str | None] = HassKey(f"{DOMAIN}_snapshots_url")
|
||||
|
||||
LABS_SNAPSHOT_FEATURE = "snapshots"
|
||||
|
||||
@@ -57,18 +62,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
analytics_config = config.get(DOMAIN, {})
|
||||
|
||||
snapshots_url: str | None = None
|
||||
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||
await labs.async_update_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
|
||||
)
|
||||
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||
else:
|
||||
snapshots_url = None
|
||||
|
||||
hass.data[_DATA_SNAPSHOTS_URL] = snapshots_url
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Analytics from a config entry."""
|
||||
snapshots_url = hass.data.get(_DATA_SNAPSHOTS_URL)
|
||||
analytics = Analytics(hass, snapshots_url)
|
||||
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
try:
|
||||
await analytics.load()
|
||||
except HassioNotReadyError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="supervisor_not_ready",
|
||||
) from err
|
||||
|
||||
started = False
|
||||
|
||||
@@ -80,26 +106,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
async def start_schedule(hass: HomeAssistant) -> None:
|
||||
"""Start the send schedule once Home Assistant has started."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
entry.async_on_unload(
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
)
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
entry.async_on_unload(async_at_started(hass, start_schedule))
|
||||
|
||||
hass.data[DATA_COMPONENT] = analytics
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload an Analytics config entry."""
|
||||
analytics = hass.data.pop(DATA_COMPONENT)
|
||||
analytics.cancel_scheduled()
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "analytics"})
|
||||
@@ -109,7 +139,9 @@ def websocket_analytics(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return analytics preferences."""
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
||||
@@ -130,8 +162,10 @@ async def websocket_analytics_preferences(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update analytics preferences."""
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
preferences = msg[ATTR_PREFERENCES]
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
|
||||
await analytics.save_preferences(preferences)
|
||||
await analytics.async_schedule()
|
||||
|
||||
@@ -299,12 +299,8 @@ class Analytics:
|
||||
self._data = AnalyticsData.from_dict(stored)
|
||||
|
||||
if self.supervisor and not self.onboarded:
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable
|
||||
# during setup of the Supervisor integration. That will fail setup
|
||||
# of this integration. However there is no better option at this time
|
||||
# since we need to get the diagnostic setting from Supervisor to correctly
|
||||
# setup this integration and we can't raise ConfigEntryNotReady to
|
||||
# trigger a retry from async_setup.
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable.
|
||||
# The caller is responsible for handling this and triggering a retry.
|
||||
supervisor_info = hassio.get_supervisor_info(self._hass)
|
||||
|
||||
# User have not configured analytics, get this setting from the supervisor
|
||||
@@ -349,8 +345,7 @@ class Analytics:
|
||||
await self._save()
|
||||
|
||||
if self.supervisor:
|
||||
# get_supervisor_info was called during setup so we can't get here
|
||||
# if it raised. The others may raise HassioNotReadyError if only some
|
||||
# The others may raise HassioNotReadyError if only some
|
||||
# data was successfully fetched from Supervisor
|
||||
supervisor_info = hassio.get_supervisor_info(hass)
|
||||
with contextlib.suppress(hassio.HassioNotReadyError):
|
||||
@@ -630,6 +625,16 @@ class Analytics:
|
||||
err,
|
||||
)
|
||||
|
||||
@callback
|
||||
def cancel_scheduled(self) -> None:
|
||||
"""Cancel all scheduled analytics tasks."""
|
||||
if self._basic_scheduled is not None:
|
||||
self._basic_scheduled()
|
||||
self._basic_scheduled = None
|
||||
if self._snapshot_scheduled is not None:
|
||||
self._snapshot_scheduled()
|
||||
self._snapshot_scheduled = None
|
||||
|
||||
async def async_schedule(self) -> None:
|
||||
"""Schedule analytics."""
|
||||
if not self.onboarded:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Config flow for Analytics integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Analytics."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_system(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
return self.async_create_entry(title="Analytics", data={})
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "Analytics",
|
||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["api", "websocket_api", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
"integration_type": "system",
|
||||
@@ -14,5 +15,6 @@
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
|
||||
}
|
||||
},
|
||||
"quality_scale": "internal"
|
||||
"quality_scale": "internal",
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"supervisor_not_ready": {
|
||||
"message": "Supervisor was not ready during setup, will retry"
|
||||
}
|
||||
},
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) – never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
|
||||
|
||||
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
|
||||
"""Implementing the Currencylayer sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by currencylayer.com"
|
||||
_attr_icon = "mdi:currency"
|
||||
_attr_icon = "mdi:currency-usd"
|
||||
|
||||
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"service": "mdi:refresh"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"service": "mdi:water-heater"
|
||||
"service": "mdi:water-boiler"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"service": "mdi:pencil"
|
||||
|
||||
@@ -16,7 +16,7 @@ class DeviceType(Enum):
|
||||
GAME_CONSOLE = "mdi:nintendo-game-boy"
|
||||
STREAMING_DONGLE = "mdi:cast"
|
||||
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
|
||||
DISC_PLAYER = "mdi:disk-player"
|
||||
DISC_PLAYER = "mdi:disc-player"
|
||||
REMOTE_CONTROL = "mdi:remote-tv"
|
||||
RADIO = "mdi:radio"
|
||||
PHOTO_CAMERA = PHOTOS = "mdi:camera"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"entity": {
|
||||
"button": {
|
||||
"sync_clock": {
|
||||
"default": "mdi:clock-sync"
|
||||
"default": "mdi:clock-check"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODE = "mode"
|
||||
ATTR_TIME_PERIOD = "time_period"
|
||||
ATTR_ONOFF = "on_off"
|
||||
CONF_CODE = "2fa"
|
||||
|
||||
@@ -12,12 +12,12 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_MODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from . import HiveConfigEntry, refresh_system
|
||||
from .const import ATTR_MODE
|
||||
from .entity import HiveEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -6,12 +6,11 @@ from typing import Any
|
||||
from apyhiveapi import Hive
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.const import ATTR_MODE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HiveConfigEntry, refresh_system
|
||||
from .const import ATTR_MODE
|
||||
from .entity import HiveEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -19,7 +19,9 @@ EXPECTED_ENTRY_VERSION = (
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entries = hass.config_entries.async_entries(
|
||||
DOMAIN, include_ignore=False, include_disabled=False
|
||||
)
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==10.0.1"],
|
||||
"requirements": ["python-homewizard-energy==10.1.0"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.variance import ignore_variance
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
|
||||
@@ -66,13 +65,6 @@ def to_percentage(value: float | None) -> float | None:
|
||||
return value * 100 if value is not None else None
|
||||
|
||||
|
||||
def uptime_to_datetime(value: int) -> datetime:
|
||||
"""Convert seconds to datetime timestamp."""
|
||||
return utcnow().replace(microsecond=0) - timedelta(seconds=value)
|
||||
|
||||
|
||||
uptime_to_stable_datetime = ignore_variance(uptime_to_datetime, timedelta(minutes=5))
|
||||
|
||||
SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
HomeWizardSensorEntityDescription(
|
||||
key="smr_version",
|
||||
@@ -643,7 +635,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
HomeWizardSensorEntityDescription(
|
||||
key="uptime",
|
||||
translation_key="uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
device_class=SensorDeviceClass.UPTIME,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
has_fn=(
|
||||
@@ -651,7 +643,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
),
|
||||
value_fn=(
|
||||
lambda data: (
|
||||
uptime_to_stable_datetime(data.system.uptime_s)
|
||||
utcnow() - timedelta(seconds=data.system.uptime_s)
|
||||
if data.system is not None and data.system.uptime_s is not None
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -61,13 +61,14 @@
|
||||
},
|
||||
"select": {
|
||||
"battery_group_mode": {
|
||||
"name": "Battery group mode",
|
||||
"name": "Battery group charging strategy",
|
||||
"state": {
|
||||
"predictive": "Smart charging",
|
||||
"standby": "Standby",
|
||||
"to_full": "Manual charge mode",
|
||||
"zero": "Zero mode",
|
||||
"zero_charge_only": "Zero mode (charge only)",
|
||||
"zero_discharge_only": "Zero mode (discharge only)"
|
||||
"to_full": "One-time full charge",
|
||||
"zero": "Net zero",
|
||||
"zero_charge_only": "Net zero (charge only)",
|
||||
"zero_discharge_only": "Net zero (discharge only)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,15 +31,16 @@ activate_scene:
|
||||
dynamic:
|
||||
selector:
|
||||
boolean:
|
||||
speed:
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
brightness:
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 255
|
||||
scene_customization:
|
||||
collapsed: true
|
||||
fields:
|
||||
speed:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
brightness:
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 255
|
||||
|
||||
@@ -184,7 +184,12 @@
|
||||
"name": "Transition"
|
||||
}
|
||||
},
|
||||
"name": "Activate Hue scene"
|
||||
"name": "Activate Hue scene",
|
||||
"sections": {
|
||||
"scene_customization": {
|
||||
"name": "Scene customization"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hue_activate_scene": {
|
||||
"description": "Activates a Hue scene stored in the Hue hub.",
|
||||
|
||||
@@ -87,6 +87,8 @@ def async_get_triggers(
|
||||
|
||||
# Get Hue device id from device identifier
|
||||
hue_dev_id = get_hue_device_id(device_entry)
|
||||
if hue_dev_id is None or hue_dev_id not in api.devices:
|
||||
return []
|
||||
# extract triggers from all button resources of this Hue device
|
||||
triggers: list[dict[str, Any]] = []
|
||||
model_id = api.devices[hue_dev_id].product_data.product_name
|
||||
|
||||
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
|
||||
|
||||
create_insteon_device(hass, devices.modem, entry.entry_id)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
|
||||
|
||||
for address in devices:
|
||||
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
register_new_device_callback(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
create_insteon_device(hass, devices.modem, entry.entry_id)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"erev_shabbat_hag": { "default": "mdi:candle-light" },
|
||||
"erev_shabbat_hag": { "default": "mdi:candle" },
|
||||
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
|
||||
"motzei_shabbat_hag": { "default": "mdi:fire" }
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"service": "mdi:lock-open"
|
||||
},
|
||||
"disable": {
|
||||
"service": "mdi:fash-off"
|
||||
"service": "mdi:flash-off"
|
||||
},
|
||||
"enable": {
|
||||
"service": "mdi:flash"
|
||||
|
||||
@@ -28,25 +28,25 @@
|
||||
"ice_maker": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_bottom_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_middle_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_top_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -241,7 +241,7 @@ def preprocess_turn_on_alternatives(
|
||||
|
||||
if (color_name := params.pop(ATTR_COLOR_NAME, None)) is not None:
|
||||
try:
|
||||
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
params[ATTR_RGB_COLOR] = tuple(color_util.color_name_to_rgb(color_name))
|
||||
except ValueError:
|
||||
_LOGGER.warning("Got unknown color %s, falling back to white", color_name)
|
||||
params[ATTR_RGB_COLOR] = (255, 255, 255)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
|
||||
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -131,7 +131,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Fetch the latest battery status from the bridge."""
|
||||
status = await self._smartbridge.get_battery_status(self.device_id)
|
||||
try:
|
||||
status = await self._smartbridge.get_battery_status(self.device_id)
|
||||
except BridgeResponseError:
|
||||
self._attr_is_on = None
|
||||
return
|
||||
normalized_status = status.strip().casefold() if status else None
|
||||
if normalized_status == BATTERY_STATUS_LOW:
|
||||
self._attr_is_on = True
|
||||
|
||||
@@ -60,7 +60,7 @@ def get_matter_device_info(
|
||||
return None
|
||||
|
||||
return MatterDeviceInfo(
|
||||
unique_id=node.device_info.uniqueID,
|
||||
unique_id=node.device_info.uniqueID or "",
|
||||
vendor_id=hex(node.device_info.vendorID),
|
||||
product_id=hex(node.device_info.productID),
|
||||
)
|
||||
|
||||
@@ -6,13 +6,14 @@ from typing import Any
|
||||
from chip.clusters import Objects
|
||||
from matter_server.common.helpers.util import dataclass_to_dict, parse_attribute_path
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .helpers import MatterConfigEntry, get_matter, get_node_from_device_entry
|
||||
|
||||
ATTRIBUTES_TO_REDACT = {Objects.BasicInformation.Attributes.Location}
|
||||
SERVER_INFO_TO_REDACT = {"wifi_ssid"}
|
||||
|
||||
|
||||
def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -44,6 +45,7 @@ async def async_get_config_entry_diagnostics(
|
||||
matter = get_matter(hass)
|
||||
server_diagnostics = await matter.matter_client.get_diagnostics()
|
||||
data = dataclass_to_dict(server_diagnostics)
|
||||
data["info"] = async_redact_data(data["info"], SERVER_INFO_TO_REDACT)
|
||||
nodes = [redact_matter_attributes(node_data) for node_data in data["nodes"]]
|
||||
data["nodes"] = nodes
|
||||
|
||||
@@ -59,7 +61,9 @@ async def async_get_device_diagnostics(
|
||||
node = get_node_from_device_entry(hass, device)
|
||||
|
||||
return {
|
||||
"server_info": dataclass_to_dict(server_diagnostics.info),
|
||||
"server_info": async_redact_data(
|
||||
dataclass_to_dict(server_diagnostics.info), SERVER_INFO_TO_REDACT
|
||||
),
|
||||
"node": redact_matter_attributes(
|
||||
remove_serialization_type(dataclass_to_dict(node.node_data) if node else {})
|
||||
),
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"eve_weather_trend": {
|
||||
"default": "mdi:weather",
|
||||
"default": "mdi:weather-cloudy",
|
||||
"state": {
|
||||
"cloudy": "mdi:weather-cloudy",
|
||||
"rainy": "mdi:weather-rainy",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["matter-python-client==0.6.0"],
|
||||
"requirements": ["matter-python-client==0.7.1"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ ABBREVIATIONS = {
|
||||
"mode_stat_t": "mode_state_topic",
|
||||
"mode_stat_tpl": "mode_state_template",
|
||||
"modes": "modes",
|
||||
"msg_exp_int": "message_expiry_interval",
|
||||
"name": "name",
|
||||
"o": "origin",
|
||||
"off_dly": "off_delay",
|
||||
|
||||
@@ -120,6 +120,8 @@ from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
DurationSelector,
|
||||
DurationSelectorConfig,
|
||||
FileSelector,
|
||||
FileSelectorConfig,
|
||||
NumberSelector,
|
||||
@@ -227,6 +229,7 @@ from .const import (
|
||||
CONF_LAST_RESET_VALUE_TEMPLATE,
|
||||
CONF_MAX,
|
||||
CONF_MAX_KELVIN,
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL,
|
||||
CONF_MIN,
|
||||
CONF_MIN_KELVIN,
|
||||
CONF_MODE_COMMAND_TEMPLATE,
|
||||
@@ -3721,6 +3724,11 @@ MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
default=DEFAULT_QOS,
|
||||
section="mqtt_settings",
|
||||
),
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL: PlatformField(
|
||||
selector=DurationSelector(DurationSelectorConfig(enable_day=True)),
|
||||
required=False,
|
||||
section="mqtt_settings",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ CONF_IMAGE_TOPIC = "image_topic"
|
||||
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
|
||||
CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template"
|
||||
CONF_KEEPALIVE = "keepalive"
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL = "message_expiry_interval"
|
||||
CONF_ORIGIN = "origin"
|
||||
CONF_QOS = ATTR_QOS
|
||||
CONF_RETAIN = ATTR_RETAIN
|
||||
|
||||
@@ -17,7 +17,7 @@ from .models import DATA_MQTT, PublishPayloadType
|
||||
STORED_MESSAGES = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TimestampedPublishMessage:
|
||||
"""MQTT Message."""
|
||||
|
||||
@@ -26,6 +26,8 @@ class TimestampedPublishMessage:
|
||||
qos: int
|
||||
retain: bool
|
||||
timestamp: float
|
||||
encoding: str | None
|
||||
kwargs: dict[str, Any]
|
||||
|
||||
|
||||
def log_message(
|
||||
@@ -35,6 +37,8 @@ def log_message(
|
||||
payload: PublishPayloadType,
|
||||
qos: int,
|
||||
retain: bool,
|
||||
encoding: str | None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Log an outgoing MQTT message."""
|
||||
entity_info = hass.data[DATA_MQTT].debug_info_entities.setdefault(
|
||||
@@ -45,7 +49,13 @@ def log_message(
|
||||
"messages": deque(maxlen=STORED_MESSAGES),
|
||||
}
|
||||
msg = TimestampedPublishMessage(
|
||||
topic, payload, qos, retain, timestamp=time.monotonic()
|
||||
topic,
|
||||
payload,
|
||||
qos,
|
||||
retain,
|
||||
timestamp=time.monotonic(),
|
||||
encoding=encoding,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
entity_info["transmitted"][topic]["messages"].append(msg)
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ from .const import (
|
||||
CONF_JSON_ATTRS_TEMPLATE,
|
||||
CONF_JSON_ATTRS_TOPIC,
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS,
|
||||
@@ -94,7 +95,6 @@ from .const import (
|
||||
CONF_SW_VERSION,
|
||||
CONF_TOPIC,
|
||||
CONF_VIA_DEVICE,
|
||||
DEFAULT_ENCODING,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTION_STATE,
|
||||
)
|
||||
@@ -153,6 +153,8 @@ MQTT_ATTRIBUTES_BLOCKED = {
|
||||
"unit_of_measurement",
|
||||
}
|
||||
|
||||
PUBLISH_KWARGS = (CONF_MESSAGE_EXPIRY_INTERVAL,)
|
||||
|
||||
|
||||
@callback
|
||||
def async_handle_schema_error(
|
||||
@@ -1539,36 +1541,20 @@ class MqttEntity(
|
||||
await MqttDiscoveryUpdateMixin.async_will_remove_from_hass(self)
|
||||
debug_info.remove_entity_data(self.hass, self.entity_id)
|
||||
|
||||
async def async_publish(
|
||||
self,
|
||||
topic: str,
|
||||
payload: PublishPayloadType,
|
||||
qos: int = 0,
|
||||
retain: bool = False,
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
) -> None:
|
||||
"""Publish message to an MQTT topic."""
|
||||
log_message(self.hass, self.entity_id, topic, payload, qos, retain)
|
||||
await async_publish(
|
||||
self.hass,
|
||||
topic,
|
||||
payload,
|
||||
qos,
|
||||
retain,
|
||||
encoding,
|
||||
)
|
||||
|
||||
async def async_publish_with_config(
|
||||
self, topic: str, payload: PublishPayloadType
|
||||
) -> None:
|
||||
"""Publish payload to a topic using config."""
|
||||
await self.async_publish(
|
||||
topic,
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
kwargs: dict[str, Any] = {
|
||||
key: value for key, value in self._config.items() if key in PUBLISH_KWARGS
|
||||
}
|
||||
qos: int = self._config[CONF_QOS]
|
||||
retain: bool = self._config[CONF_RETAIN]
|
||||
encoding: str = self._config[CONF_ENCODING]
|
||||
log_message(
|
||||
self.hass, self.entity_id, topic, payload, qos, retain, encoding, **kwargs
|
||||
)
|
||||
await async_publish(self.hass, topic, payload, qos, retain, encoding, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
|
||||
@@ -509,10 +509,20 @@ class MqttComponentConfig:
|
||||
discovery_payload: MQTTDiscoveryPayload
|
||||
|
||||
|
||||
class MessageExpiryInterval(TypedDict, total=False):
|
||||
"""Hold the Message Expiry Interval."""
|
||||
|
||||
days: float
|
||||
hours: float
|
||||
minutes: float
|
||||
seconds: float
|
||||
|
||||
|
||||
class DeviceMqttOptions(TypedDict, total=False):
|
||||
"""Hold the shared MQTT specific options for an MQTT device."""
|
||||
|
||||
qos: int
|
||||
message_expiry_interval: MessageExpiryInterval
|
||||
|
||||
|
||||
class MqttDeviceData(TypedDict, total=False):
|
||||
|
||||
@@ -40,6 +40,7 @@ from .const import (
|
||||
CONF_JSON_ATTRS_TEMPLATE,
|
||||
CONF_JSON_ATTRS_TOPIC,
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL,
|
||||
CONF_ORIGIN,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
@@ -66,6 +67,7 @@ SHARED_OPTIONS = [
|
||||
CONF_AVAILABILITY_TOPIC,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_MESSAGE_EXPIRY_INTERVAL,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -161,6 +163,14 @@ MQTT_ORIGIN_INFO_SCHEMA = vol.All(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def valid_message_expiry_interval(value: Any) -> int:
|
||||
"""Return Message Expiry Interval in seconds."""
|
||||
if isinstance(value, int):
|
||||
return cv.positive_int(value) # type: ignore[no-any-return]
|
||||
return int(cv.positive_time_period_dict(value).total_seconds())
|
||||
|
||||
|
||||
MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
@@ -172,6 +182,7 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
|
||||
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
|
||||
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
@@ -203,6 +214,7 @@ DEVICE_DISCOVERY_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
|
||||
vol.Required(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
|
||||
vol.Optional(CONF_QOS): valid_qos_schema,
|
||||
vol.Optional(CONF_ENCODING): cv.string,
|
||||
}
|
||||
|
||||
@@ -197,9 +197,11 @@
|
||||
},
|
||||
"mqtt_settings": {
|
||||
"data": {
|
||||
"message_expiry_interval": "Message Expiry Interval",
|
||||
"qos": "QoS"
|
||||
},
|
||||
"data_description": {
|
||||
"message_expiry_interval": "Retention time interval for published message.",
|
||||
"qos": "The Quality of Service value the device's entities should use."
|
||||
},
|
||||
"name": "MQTT settings"
|
||||
|
||||
@@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -14,7 +15,6 @@ from .const import (
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_EXPIRES,
|
||||
ATTR_HEADLINE,
|
||||
ATTR_ID,
|
||||
ATTR_RECOMMENDED_ACTIONS,
|
||||
ATTR_SENDER,
|
||||
ATTR_SENT,
|
||||
|
||||
@@ -29,8 +29,6 @@ ATTR_SEVERITY: str = "severity"
|
||||
ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions"
|
||||
ATTR_AFFECTED_AREAS: str = "affected_areas"
|
||||
ATTR_WEB: str = "web"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_ID: str = "id"
|
||||
ATTR_SENT: str = "sent"
|
||||
ATTR_START: str = "start"
|
||||
ATTR_EXPIRES: str = "expires"
|
||||
|
||||
@@ -595,8 +595,8 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
)
|
||||
)
|
||||
|
||||
if "reasoning" not in model_args:
|
||||
# Reasoning models handle this correctly with just a prompt
|
||||
if not model_args["model"].startswith("o"):
|
||||
# o-series models handle this correctly with just a prompt
|
||||
remove_citations = True
|
||||
|
||||
tools.append(web_search)
|
||||
|
||||
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
|
||||
return self.async_abort(reason="incomplete_discovery")
|
||||
|
||||
_LOGGER.debug(
|
||||
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
|
||||
)
|
||||
udn = discovery_info.upnp[ATTR_UPNP_UDN]
|
||||
if isinstance(udn, list):
|
||||
if not udn:
|
||||
return self.async_abort(reason="incomplete_discovery")
|
||||
udn = udn[0]
|
||||
|
||||
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
|
||||
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
|
||||
|
||||
await self.async_set_unique_id(udn)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -29,29 +29,29 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"translation_key_0": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_clicks_cubic_meter": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_1": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_consumed_liters": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_2": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_leak_clicks": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_3": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_leak_volume": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_4": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_start_index": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_5": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_watering_clicks": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_6": {
|
||||
"default": "mdi:abc"
|
||||
"last_leak_detected": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_7": {
|
||||
"default": "mdi:abc"
|
||||
"rain_sensor_rain_start": {
|
||||
"default": "mdi:weather-pouring"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.19.1"]
|
||||
"requirements": ["reolink-aio==0.20.0"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.const import CONF_REGION, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -38,7 +38,6 @@ from . import RoborockConfigEntry
|
||||
from .const import (
|
||||
CONF_BASE_URL,
|
||||
CONF_ENTRY_CODE,
|
||||
CONF_REGION,
|
||||
CONF_SHOW_BACKGROUND,
|
||||
CONF_SHOW_ROOMS,
|
||||
CONF_SHOW_WALLS,
|
||||
|
||||
@@ -13,8 +13,6 @@ CONF_USER_DATA = "user_data"
|
||||
CONF_SHOW_BACKGROUND = "show_background"
|
||||
CONF_SHOW_WALLS = "show_walls"
|
||||
CONF_SHOW_ROOMS = "show_rooms"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_REGION = "region"
|
||||
REGION_OPTIONS = ["auto", "us", "eu", "ru", "cn"]
|
||||
|
||||
# Option Flow steps
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""The System Bridge integration."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
@@ -11,71 +9,34 @@ from systembridgeconnector.exceptions import (
|
||||
ConnectionErrorException,
|
||||
DataMissingException,
|
||||
)
|
||||
from systembridgeconnector.models.keyboard_key import KeyboardKey
|
||||
from systembridgeconnector.models.keyboard_text import KeyboardText
|
||||
from systembridgeconnector.models.modules.processes import Process
|
||||
from systembridgeconnector.models.open_path import OpenPath
|
||||
from systembridgeconnector.models.open_url import OpenUrl
|
||||
from systembridgeconnector.version import Version
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COMMAND,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_TOKEN,
|
||||
CONF_URL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config_flow import SystemBridgeConfigFlow
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
|
||||
|
||||
def _get_coordinator(
|
||||
hass: HomeAssistant, entry_id: str
|
||||
) -> SystemBridgeDataUpdateCoordinator:
|
||||
"""Return the coordinator for a config entry id."""
|
||||
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
entry_id
|
||||
)
|
||||
if entry is None or entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": entry_id},
|
||||
)
|
||||
return entry.runtime_data
|
||||
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.MEDIA_PLAYER,
|
||||
@@ -84,26 +45,12 @@ PLATFORMS = [
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
CONF_BRIDGE = "bridge"
|
||||
CONF_KEY = "key"
|
||||
CONF_TEXT = "text"
|
||||
|
||||
SERVICE_GET_PROCESS_BY_ID = "get_process_by_id"
|
||||
SERVICE_GET_PROCESSES_BY_NAME = "get_processes_by_name"
|
||||
SERVICE_OPEN_PATH = "open_path"
|
||||
SERVICE_POWER_COMMAND = "power_command"
|
||||
SERVICE_OPEN_URL = "open_url"
|
||||
SERVICE_SEND_KEYPRESS = "send_keypress"
|
||||
SERVICE_SEND_TEXT = "send_text"
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the System Bridge services."""
|
||||
|
||||
POWER_COMMAND_MAP = {
|
||||
"hibernate": "power_hibernate",
|
||||
"lock": "power_lock",
|
||||
"logout": "power_logout",
|
||||
"restart": "power_restart",
|
||||
"shutdown": "power_shutdown",
|
||||
"sleep": "power_sleep",
|
||||
}
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -231,219 +178,6 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
|
||||
return True
|
||||
|
||||
def valid_device(device: str) -> str:
|
||||
"""Check device is valid."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device)
|
||||
if device_entry is not None:
|
||||
try:
|
||||
return next(
|
||||
entry.entry_id
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
)
|
||||
except StopIteration as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": device},
|
||||
) from exception
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": device},
|
||||
)
|
||||
|
||||
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the get process by id service call."""
|
||||
_LOGGER.debug("Get process by id: %s", service_call.data)
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
processes: list[Process] = coordinator.data.processes
|
||||
|
||||
# Find process.id from list, raise ServiceValidationError if not found
|
||||
try:
|
||||
return asdict(
|
||||
next(
|
||||
process
|
||||
for process in processes
|
||||
if process.id == service_call.data[CONF_ID]
|
||||
)
|
||||
)
|
||||
except StopIteration as exception:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="process_not_found",
|
||||
translation_placeholders={"id": service_call.data[CONF_ID]},
|
||||
) from exception
|
||||
|
||||
async def handle_get_processes_by_name(
|
||||
service_call: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Handle the get process by name service call."""
|
||||
_LOGGER.debug("Get process by name: %s", service_call.data)
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
|
||||
# Find processes from list
|
||||
items: list[dict[str, Any]] = [
|
||||
asdict(process)
|
||||
for process in coordinator.data.processes
|
||||
if process.name is not None
|
||||
and service_call.data[CONF_NAME].lower() in process.name.lower()
|
||||
]
|
||||
|
||||
return {
|
||||
"count": len(items),
|
||||
"processes": list(items),
|
||||
}
|
||||
|
||||
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open path service call."""
|
||||
_LOGGER.debug("Open path: %s", service_call.data)
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_path(
|
||||
OpenPath(path=service_call.data[CONF_PATH])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the power command service call."""
|
||||
_LOGGER.debug("Power command: %s", service_call.data)
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await getattr(
|
||||
coordinator.websocket_client,
|
||||
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
|
||||
)()
|
||||
return asdict(response)
|
||||
|
||||
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open url service call."""
|
||||
_LOGGER.debug("Open URL: %s", service_call.data)
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_url(
|
||||
OpenUrl(url=service_call.data[CONF_URL])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_keypress(
|
||||
KeyboardKey(key=service_call.data[CONF_KEY])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_text(
|
||||
KeyboardText(text=service_call.data[CONF_TEXT])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_PROCESS_BY_ID,
|
||||
handle_get_process_by_id,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_ID): cv.positive_int,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_PROCESSES_BY_NAME,
|
||||
handle_get_processes_by_name,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_OPEN_PATH,
|
||||
handle_open_path,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_PATH): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_POWER_COMMAND,
|
||||
handle_power_command,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_OPEN_URL,
|
||||
handle_open_url,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_KEYPRESS,
|
||||
handle_send_keypress,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
|
||||
},
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TEXT,
|
||||
handle_send_text,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): valid_device,
|
||||
vol.Required(CONF_TEXT): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
# Reload entry when its updated.
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
@@ -454,9 +188,7 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SystemBridgeConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Service registration for System Bridge integration."""
|
||||
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from systembridgeconnector.models.keyboard_key import KeyboardKey
|
||||
from systembridgeconnector.models.keyboard_text import KeyboardText
|
||||
from systembridgeconnector.models.modules.processes import Process
|
||||
from systembridgeconnector.models.open_path import OpenPath
|
||||
from systembridgeconnector.models.open_url import OpenUrl
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_COMMAND, CONF_ID, CONF_NAME, CONF_PATH, CONF_URL
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
service,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BRIDGE = "bridge"
|
||||
CONF_KEY = "key"
|
||||
CONF_TEXT = "text"
|
||||
|
||||
POWER_COMMAND_MAP = {
|
||||
"hibernate": "power_hibernate",
|
||||
"lock": "power_lock",
|
||||
"logout": "power_logout",
|
||||
"restart": "power_restart",
|
||||
"shutdown": "power_shutdown",
|
||||
"sleep": "power_sleep",
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for System Bridge integration."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"get_process_by_id",
|
||||
handle_get_process_by_id,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_ID): cv.positive_int,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"get_processes_by_name",
|
||||
handle_get_processes_by_name,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"open_path",
|
||||
handle_open_path,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_PATH): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"power_command",
|
||||
handle_power_command,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"open_url",
|
||||
handle_open_url,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"send_keypress",
|
||||
handle_send_keypress,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
|
||||
},
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"send_text",
|
||||
handle_send_text,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BRIDGE): cv.string,
|
||||
vol.Required(CONF_TEXT): cv.string,
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
def _get_coordinator(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> SystemBridgeDataUpdateCoordinator:
|
||||
"""Return the coordinator for a device id."""
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": device_id},
|
||||
)
|
||||
try:
|
||||
entry_id = next(
|
||||
entry.entry_id
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": device_id},
|
||||
) from e
|
||||
entry: SystemBridgeConfigEntry = service.async_get_config_entry(
|
||||
hass, DOMAIN, entry_id
|
||||
)
|
||||
return entry.runtime_data
|
||||
|
||||
|
||||
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the get process by id service call."""
|
||||
_LOGGER.debug("Get process by id: %s", service_call.data)
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
processes: list[Process] = coordinator.data.processes
|
||||
|
||||
# Find process.id from list, raise ServiceValidationError if not found
|
||||
try:
|
||||
return asdict(
|
||||
next(
|
||||
process
|
||||
for process in processes
|
||||
if process.id == service_call.data[CONF_ID]
|
||||
)
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="process_not_found",
|
||||
translation_placeholders={"id": service_call.data[CONF_ID]},
|
||||
) from e
|
||||
|
||||
|
||||
async def handle_get_processes_by_name(
|
||||
service_call: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Handle the get process by name service call."""
|
||||
_LOGGER.debug("Get process by name: %s", service_call.data)
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
|
||||
# Find processes from list
|
||||
items: list[dict[str, Any]] = [
|
||||
asdict(process)
|
||||
for process in coordinator.data.processes
|
||||
if process.name is not None
|
||||
and service_call.data[CONF_NAME].lower() in process.name.lower()
|
||||
]
|
||||
|
||||
return {
|
||||
"count": len(items),
|
||||
"processes": list(items),
|
||||
}
|
||||
|
||||
|
||||
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open path service call."""
|
||||
_LOGGER.debug("Open path: %s", service_call.data)
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_path(
|
||||
OpenPath(path=service_call.data[CONF_PATH])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
|
||||
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the power command service call."""
|
||||
_LOGGER.debug("Power command: %s", service_call.data)
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
response = await getattr(
|
||||
coordinator.websocket_client,
|
||||
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
|
||||
)()
|
||||
return asdict(response)
|
||||
|
||||
|
||||
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open url service call."""
|
||||
_LOGGER.debug("Open URL: %s", service_call.data)
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_url(
|
||||
OpenUrl(url=service_call.data[CONF_URL])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
|
||||
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_keypress(
|
||||
KeyboardKey(key=service_call.data[CONF_KEY])
|
||||
)
|
||||
return asdict(response)
|
||||
|
||||
|
||||
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_text service call."""
|
||||
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_text(
|
||||
KeyboardText(text=service_call.data[CONF_TEXT])
|
||||
)
|
||||
return asdict(response)
|
||||
@@ -89,3 +89,4 @@ power_command:
|
||||
- "restart"
|
||||
- "shutdown"
|
||||
- "sleep"
|
||||
translation_key: "power_command"
|
||||
|
||||
@@ -178,6 +178,18 @@
|
||||
"title": "System Bridge upgrade required"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"power_command": {
|
||||
"options": {
|
||||
"hibernate": "Hibernate",
|
||||
"lock": "Lock",
|
||||
"logout": "Logout",
|
||||
"restart": "[%key:common::action::restart%]",
|
||||
"shutdown": "Shutdown",
|
||||
"sleep": "Sleep"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_process_by_id": {
|
||||
"description": "Gets a process by the ID.",
|
||||
|
||||
@@ -58,7 +58,7 @@ send_message:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -101,7 +101,7 @@ send_chat_action:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -195,7 +195,7 @@ send_photo:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -287,7 +287,7 @@ send_media_group:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -372,7 +372,7 @@ send_sticker:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -466,7 +466,7 @@ send_animation:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -560,7 +560,7 @@ send_video:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -645,7 +645,7 @@ send_voice:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -739,7 +739,7 @@ send_document:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -804,7 +804,7 @@ send_location:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -861,7 +861,7 @@ send_poll:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -913,7 +913,7 @@ edit_message:
|
||||
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -991,7 +991,7 @@ edit_message_media:
|
||||
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1028,7 +1028,7 @@ edit_caption:
|
||||
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1061,7 +1061,7 @@ edit_replymarkup:
|
||||
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1108,7 +1108,7 @@ delete_message:
|
||||
example: "{{ trigger.event.data.message.message_id }}"
|
||||
selector:
|
||||
text:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1129,7 +1129,7 @@ leave_chat:
|
||||
filter:
|
||||
domain: notify
|
||||
integration: telegram_bot
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1164,7 +1164,7 @@ set_message_reaction:
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
@@ -1233,7 +1233,7 @@ send_message_draft:
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
advanced:
|
||||
additional_fields:
|
||||
collapsed: true
|
||||
fields:
|
||||
config_entry_id:
|
||||
|
||||
@@ -367,8 +367,8 @@
|
||||
},
|
||||
"name": "Delete message",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -425,8 +425,8 @@
|
||||
},
|
||||
"name": "Edit caption",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -472,8 +472,8 @@
|
||||
},
|
||||
"name": "Edit message",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -535,8 +535,8 @@
|
||||
},
|
||||
"name": "Edit message media",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -569,8 +569,8 @@
|
||||
},
|
||||
"name": "Edit reply markup",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -592,8 +592,8 @@
|
||||
},
|
||||
"name": "Leave chat",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -671,8 +671,8 @@
|
||||
},
|
||||
"name": "Send animation",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -705,8 +705,8 @@
|
||||
},
|
||||
"name": "Send chat action",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -784,8 +784,8 @@
|
||||
},
|
||||
"name": "Send document",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -842,8 +842,8 @@
|
||||
},
|
||||
"name": "Send location",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -889,8 +889,8 @@
|
||||
},
|
||||
"name": "Send media group",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -952,8 +952,8 @@
|
||||
},
|
||||
"name": "Send message",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "Advanced"
|
||||
"additional_fields": {
|
||||
"name": "Additional options"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -991,8 +991,8 @@
|
||||
},
|
||||
"name": "Send message draft",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1070,8 +1070,8 @@
|
||||
},
|
||||
"name": "Send photo",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "Advanced"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "URL options"
|
||||
@@ -1128,8 +1128,8 @@
|
||||
},
|
||||
"name": "Send poll",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1203,8 +1203,8 @@
|
||||
},
|
||||
"name": "Send sticker",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -1285,8 +1285,8 @@
|
||||
},
|
||||
"name": "Send video",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -1363,8 +1363,8 @@
|
||||
},
|
||||
"name": "Send voice",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
},
|
||||
"url_options": {
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
|
||||
@@ -1401,8 +1401,8 @@
|
||||
},
|
||||
"name": "Set message reaction",
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
|
||||
"additional_fields": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
"default": "mdi:battery-clock"
|
||||
},
|
||||
"forward_collision_warning": {
|
||||
"default": "mdi:car-crash",
|
||||
"default": "mdi:car-emergency",
|
||||
"state": {
|
||||
"average": "mdi:alert-circle",
|
||||
"early": "mdi:alert-octagon",
|
||||
@@ -634,7 +634,7 @@
|
||||
"default": "mdi:key"
|
||||
},
|
||||
"pedal_position": {
|
||||
"default": "mdi:pedestal"
|
||||
"default": "mdi:gauge"
|
||||
},
|
||||
"powershare_hours_left": {
|
||||
"default": "mdi:clock-time-eight-outline"
|
||||
@@ -794,7 +794,7 @@
|
||||
"service": "mdi:calendar-plus"
|
||||
},
|
||||
"add_precondition_schedule": {
|
||||
"service": "mdi:hvac-outline"
|
||||
"service": "mdi:hvac"
|
||||
},
|
||||
"navigation_gps_request": {
|
||||
"service": "mdi:crosshairs-gps"
|
||||
@@ -803,7 +803,7 @@
|
||||
"service": "mdi:calendar-minus"
|
||||
},
|
||||
"remove_precondition_schedule": {
|
||||
"service": "mdi:hvac-off-outline"
|
||||
"service": "mdi:hvac-off"
|
||||
},
|
||||
"set_scheduled_charging": {
|
||||
"service": "mdi:timeline-clock-outline"
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import PLATFORMS
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import UptimeRobotConfigEntry, UptimeRobotDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeRobotConfigEntry)
|
||||
"""Set up UptimeRobot from a config entry."""
|
||||
key: str = entry.data[CONF_API_KEY]
|
||||
if key.startswith(("ur", "m")):
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Wrong API key type detected, use the 'main' API key"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_key_wrong_type",
|
||||
)
|
||||
uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass))
|
||||
|
||||
|
||||
@@ -48,11 +48,16 @@ class UptimeRobotDataUpdateCoordinator(
|
||||
try:
|
||||
response = await self.api.async_get_monitors()
|
||||
except UptimeRobotAuthenticationException as exception:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryAuthFailed(exception) from exception
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_exception",
|
||||
) from exception
|
||||
except UptimeRobotException as exception:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise UpdateFailed(exception) from exception
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_generic_exception",
|
||||
translation_placeholders={"error": "Generic UptimeRobot exception"},
|
||||
) from exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(response.data, list)
|
||||
|
||||
@@ -57,7 +57,16 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_exception": {
|
||||
"api_authentication_exception": {
|
||||
"message": "API authentication failed, please check your API key"
|
||||
},
|
||||
"api_generic_exception": {
|
||||
"message": "API error: {error}"
|
||||
},
|
||||
"api_key_wrong_type": {
|
||||
"message": "Wrong API key type detected, use the 'main' API key"
|
||||
},
|
||||
"api_switch_exception": {
|
||||
"message": "Could not turn on/off monitoring: {error}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ def uptimerobot_api_call[_T: UptimeRobotEntity, **_P](
|
||||
except UptimeRobotException as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_exception",
|
||||
translation_key="api_switch_exception",
|
||||
translation_placeholders={"error": "Generic UptimeRobot exception"},
|
||||
) from exception
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"state": {
|
||||
"lightning": "mdi:weather-lightning-rainy",
|
||||
"rain": "mdi:weather-rainy",
|
||||
"rain_snow": "mdi:weather-snoy-rainy",
|
||||
"rain_snow": "mdi:weather-snowy-rainy",
|
||||
"snow": "mdi:weather-snowy"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ from miio.integrations.humidifier.zhimi.airhumidifier_miot import (
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory
|
||||
from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -65,8 +65,6 @@ from .typing import XiaomiMiioConfigEntry
|
||||
ATTR_DISPLAY_ORIENTATION = "display_orientation"
|
||||
ATTR_LED_BRIGHTNESS = "led_brightness"
|
||||
ATTR_PTC_LEVEL = "ptc_level"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODE = "mode"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_MODEL,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
@@ -149,8 +150,6 @@ ATTR_LED = "led"
|
||||
ATTR_IONIZER = "ionizer"
|
||||
ATTR_ANION = "anion"
|
||||
ATTR_LOAD_POWER = "load_power"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_POWER = "power"
|
||||
ATTR_POWER_MODE = "power_mode"
|
||||
ATTR_POWER_PRICE = "power_price"
|
||||
|
||||
@@ -21,4 +21,4 @@ CONF_INSTANCE_ID = "instance_id"
|
||||
# Polling interval (seconds)
|
||||
DEFAULT_SCAN_INTERVAL = 1800
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.SWITCH]
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.LOCK, Platform.SWITCH]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Lock platform for Xthings Cloud."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import XthingsCloudConfigEntry
|
||||
from .entity import XthingsCloudEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: XthingsCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up lock platform."""
|
||||
coordinator = entry.runtime_data
|
||||
entities = [
|
||||
XthingsCloudLock(coordinator, device_id, device_data)
|
||||
for device_id, device_data in coordinator.data.items()
|
||||
if device_data["type"] == "lock"
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class XthingsCloudLock(XthingsCloudEntity, LockEntity):
|
||||
"""Xthings Cloud lock entity."""
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool | None:
|
||||
"""Return true if lock is locked."""
|
||||
return self.device_data["status"].get("locked")
|
||||
|
||||
@property
|
||||
def is_jammed(self) -> bool | None:
|
||||
"""Return true if lock is jammed."""
|
||||
return self.device_data["status"].get("jammed")
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the device."""
|
||||
await self.coordinator.client.async_lock_lock(self._device_id)
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the device."""
|
||||
await self.coordinator.client.async_lock_unlock(self._device_id)
|
||||
Generated
+1
@@ -59,6 +59,7 @@ FLOWS = {
|
||||
"amberelectric",
|
||||
"ambient_network",
|
||||
"ambient_station",
|
||||
"analytics",
|
||||
"analytics_insights",
|
||||
"android_ip_webcam",
|
||||
"androidtv",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Checker for invalid MDI icon references.
|
||||
|
||||
Validates that ``mdi:`` icon references in integration code and
|
||||
``icons.json`` files refer to icons that actually exist in the
|
||||
Material Design Icons set.
|
||||
|
||||
- ``E7409``: MDI icon reference not found in Python code
|
||||
- ``E7410``: MDI icon reference not found in icons.json
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.generated.mdi_icons import MDI_ICONS
|
||||
from pylint_home_assistant.helpers.icons import collect_mdi_icons, load_icons
|
||||
from pylint_home_assistant.helpers.module_info import parse_module
|
||||
|
||||
# Matches strings that look like intentional icon name attempts
|
||||
# (letters, digits, hyphens, underscores). Rejects format templates
|
||||
# (%s, {}, {name}), empty names, and other dynamic patterns.
|
||||
_LOOKS_LIKE_ICON_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9]$")
|
||||
|
||||
|
||||
class MdiIconsChecker(BaseChecker):
|
||||
"""Checker for invalid MDI icon references."""
|
||||
|
||||
name = "home_assistant_mdi_icons"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"E7409": (
|
||||
"MDI icon '%s' does not exist in the Material Design Icons set",
|
||||
"home-assistant-mdi-icon-not-found",
|
||||
"Used when an integration references an MDI icon in Python "
|
||||
"code that does not exist. Check the icon name at "
|
||||
"https://pictogrammers.com/library/mdi/",
|
||||
),
|
||||
"E7410": (
|
||||
"MDI icon '%s' in icons.json does not exist in the "
|
||||
"Material Design Icons set",
|
||||
"home-assistant-mdi-icon-json-not-found",
|
||||
"Used when an integration's icons.json references an MDI "
|
||||
"icon that does not exist. Check the icon name at "
|
||||
"https://pictogrammers.com/library/mdi/",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_in_integration: bool
|
||||
_checked_icons_json: set[str]
|
||||
|
||||
def open(self) -> None:
|
||||
"""Initialize per-run state."""
|
||||
self._checked_icons_json = set()
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Check icons.json and track integration context."""
|
||||
parsed = parse_module(node.name)
|
||||
self._in_integration = parsed is not None
|
||||
if parsed is None:
|
||||
return
|
||||
|
||||
# Only check icons.json once per integration
|
||||
if parsed.domain in self._checked_icons_json:
|
||||
return
|
||||
self._checked_icons_json.add(parsed.domain)
|
||||
|
||||
icons_data = load_icons(node)
|
||||
if icons_data is None:
|
||||
return
|
||||
|
||||
mdi_refs = collect_mdi_icons(icons_data)
|
||||
for icon_ref in sorted(mdi_refs):
|
||||
icon_name = icon_ref[4:] # Strip "mdi:" prefix
|
||||
if icon_name not in MDI_ICONS:
|
||||
self.add_message(
|
||||
"home-assistant-mdi-icon-json-not-found",
|
||||
node=node,
|
||||
args=(icon_ref,),
|
||||
)
|
||||
|
||||
def visit_const(self, node: nodes.Const) -> None:
|
||||
"""Check string constants for invalid MDI icon references."""
|
||||
if not self._in_integration:
|
||||
return
|
||||
|
||||
if not isinstance(node.value, str):
|
||||
return
|
||||
|
||||
if not node.value.startswith("mdi:"):
|
||||
return
|
||||
|
||||
icon_name = node.value[4:] # Strip "mdi:" prefix
|
||||
|
||||
# Only check names that look like intentional icon name attempts.
|
||||
# This skips f-string fragments, format templates (%s, {}),
|
||||
# partial names, and other dynamic patterns.
|
||||
if not _LOOKS_LIKE_ICON_NAME.match(icon_name):
|
||||
return
|
||||
|
||||
if icon_name not in MDI_ICONS:
|
||||
self.add_message(
|
||||
"home-assistant-mdi-icon-not-found",
|
||||
node=node,
|
||||
args=(node.value,),
|
||||
)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(MdiIconsChecker(linter))
|
||||
@@ -0,0 +1 @@
|
||||
"""Generated files for the pylint Home Assistant plugin."""
|
||||
+7458
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
"""Helpers for reading integration icon files."""
|
||||
|
||||
import contextlib
|
||||
|
||||
from astroid import nodes
|
||||
import orjson
|
||||
|
||||
from .integration import get_integration_dir
|
||||
|
||||
_icons_cache: dict[str, dict | None] = {}
|
||||
|
||||
|
||||
def clear_icons_cache() -> None:
|
||||
"""Clear the icons cache (used by tests)."""
|
||||
_icons_cache.clear()
|
||||
|
||||
|
||||
def load_icons(module: nodes.Module) -> dict | None:
|
||||
"""Load and cache the icons.json for the current integration.
|
||||
|
||||
Returns the parsed JSON as a dict, or ``None`` if not found.
|
||||
"""
|
||||
integration_dir = get_integration_dir(module)
|
||||
if integration_dir is None:
|
||||
return None
|
||||
|
||||
cache_key = str(integration_dir)
|
||||
if cache_key in _icons_cache:
|
||||
return _icons_cache[cache_key]
|
||||
|
||||
icons_path = integration_dir / "icons.json"
|
||||
result: dict | None = None
|
||||
if icons_path.exists():
|
||||
with contextlib.suppress(orjson.JSONDecodeError, OSError):
|
||||
parsed = orjson.loads(icons_path.read_bytes())
|
||||
if isinstance(parsed, dict):
|
||||
result = parsed
|
||||
|
||||
_icons_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
|
||||
def collect_mdi_icons(
|
||||
data: dict | list | str, icons: set[str] | None = None
|
||||
) -> set[str]:
|
||||
"""Recursively collect all mdi: icon references from a data structure."""
|
||||
if icons is None:
|
||||
icons = set()
|
||||
|
||||
if isinstance(data, str):
|
||||
if data.startswith("mdi:"):
|
||||
icons.add(data)
|
||||
elif isinstance(data, dict):
|
||||
for value in data.values():
|
||||
collect_mdi_icons(value, icons)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
collect_mdi_icons(item, icons)
|
||||
|
||||
return icons
|
||||
Generated
+3
-3
@@ -1516,7 +1516,7 @@ lxml==6.0.1
|
||||
matrix-nio==0.25.2
|
||||
|
||||
# homeassistant.components.matter
|
||||
matter-python-client==0.6.0
|
||||
matter-python-client==0.7.1
|
||||
|
||||
# homeassistant.components.maxcube
|
||||
maxcube-api==0.4.3
|
||||
@@ -2641,7 +2641,7 @@ python-google-weather-api==0.0.6
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==10.0.1
|
||||
python-homewizard-energy==10.1.0
|
||||
|
||||
# homeassistant.components.hp_ilo
|
||||
python-hpilo==4.4.3
|
||||
@@ -2871,7 +2871,7 @@ renault-api==0.5.10
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.19.1
|
||||
reolink-aio==0.20.0
|
||||
|
||||
# homeassistant.components.radio_frequency
|
||||
rf-protocols==3.2.0
|
||||
|
||||
@@ -23,6 +23,7 @@ from . import (
|
||||
json,
|
||||
labs,
|
||||
manifest,
|
||||
mdi_icons,
|
||||
metadata,
|
||||
mqtt,
|
||||
mypy_config,
|
||||
@@ -65,6 +66,7 @@ INTEGRATION_PLUGINS = [
|
||||
HASS_PLUGINS = [
|
||||
core_files,
|
||||
docker,
|
||||
mdi_icons,
|
||||
mypy_config,
|
||||
metadata,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Generate MDI icons file for the pylint plugin."""
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from importlib.resources import files
|
||||
import json
|
||||
|
||||
from .model import Config, Integration
|
||||
from .serializer import format_python_namespace
|
||||
|
||||
_TARGET = "pylint/plugins/pylint_home_assistant/generated/mdi_icons.py"
|
||||
|
||||
|
||||
def _get_frontend_version() -> str | None:
|
||||
"""Get the installed home-assistant-frontend version."""
|
||||
try:
|
||||
return version("home-assistant-frontend")
|
||||
except PackageNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def _load_mdi_icons() -> set[str]:
|
||||
"""Load the MDI icon names from the frontend package."""
|
||||
try:
|
||||
mdi_dir = files("hass_frontend") / "static" / "mdi"
|
||||
icon_list_path = mdi_dir / "iconList.json"
|
||||
data = json.loads(icon_list_path.read_text(encoding="utf-8"))
|
||||
return {icon["name"] for icon in data}
|
||||
except ImportError, FileNotFoundError, json.JSONDecodeError, KeyError:
|
||||
return set()
|
||||
|
||||
|
||||
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
"""Validate the generated MDI icons file is up to date."""
|
||||
frontend_version = _get_frontend_version()
|
||||
if frontend_version is None:
|
||||
return
|
||||
|
||||
icons = _load_mdi_icons()
|
||||
if not icons:
|
||||
config.add_error(
|
||||
"mdi_icons",
|
||||
"Could not load MDI icons from home-assistant-frontend",
|
||||
)
|
||||
return
|
||||
|
||||
content = format_python_namespace(
|
||||
{
|
||||
"FRONTEND_VERSION": frontend_version,
|
||||
"MDI_ICONS": icons,
|
||||
},
|
||||
annotations={
|
||||
"FRONTEND_VERSION": "Final[str]",
|
||||
"MDI_ICONS": "Final[set[str]]",
|
||||
},
|
||||
)
|
||||
|
||||
config.cache["mdi_icons_content"] = content
|
||||
|
||||
if config.specific_integrations:
|
||||
return
|
||||
|
||||
target_path = config.root / _TARGET
|
||||
if not target_path.exists() or target_path.read_text() != content:
|
||||
config.add_error(
|
||||
"mdi_icons",
|
||||
f"File {_TARGET} is not up to date. Run python3 -m script.hassfest",
|
||||
fixable=True,
|
||||
)
|
||||
|
||||
|
||||
def generate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
"""Generate MDI icons file."""
|
||||
if "mdi_icons_content" not in config.cache:
|
||||
return
|
||||
target_path = config.root / _TARGET
|
||||
target_path.write_text(config.cache["mdi_icons_content"])
|
||||
@@ -102,37 +102,37 @@ async def test_login(hass: HomeAssistant) -> None:
|
||||
|
||||
provider = hass.auth.auth_providers[0]
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("pin") is str
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"pin": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"pin": "123456"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ async def test_setup_flow(hass: HomeAssistant) -> None:
|
||||
flow = await auth_module.async_setup_flow("new-user")
|
||||
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init({"pin": "abcdefg"})
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert auth_module._data[1]["user_id"] == "new-user"
|
||||
assert auth_module._data[1]["pin"] == "abcdefg"
|
||||
|
||||
@@ -137,25 +137,25 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -173,7 +173,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -191,7 +191,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -199,7 +199,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "too_many_retry"
|
||||
|
||||
# wait service call finished
|
||||
@@ -207,13 +207,13 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
|
||||
# restart login
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": MOCK_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
flow = await notify_auth_module.async_setup_flow("test-user")
|
||||
step = await flow.async_step_init()
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "init"
|
||||
schema = step["data_schema"]
|
||||
schema({"notify_service": "test2"})
|
||||
@@ -277,7 +277,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
step = await flow.async_step_init({"notify_service": "test1"})
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "setup"
|
||||
|
||||
# wait service call finished
|
||||
@@ -357,7 +357,7 @@ async def test_setup_user_no_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
flow = await notify_auth_module.async_setup_flow("test-user")
|
||||
step = await flow.async_step_init()
|
||||
assert step["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert step["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert step["reason"] == "no_available_service"
|
||||
|
||||
|
||||
@@ -394,13 +394,13 @@ async def test_not_raise_exception_when_service_not_exist(hass: HomeAssistant) -
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown_error"
|
||||
|
||||
# wait service call finished
|
||||
|
||||
@@ -95,24 +95,24 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -120,7 +120,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -128,7 +128,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": MOCK_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
|
||||
@@ -139,18 +139,18 @@ async def test_login_flow_validates(
|
||||
"""Test login flow."""
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "bad-user", "password": "bad-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "good-user", "password": "good-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "good-user"
|
||||
|
||||
|
||||
@@ -160,5 +160,5 @@ async def test_strip_username(provider: command_line.CommandLineAuthProvider) ->
|
||||
result = await flow.async_step_init(
|
||||
{"username": "\t\ngood-user ", "password": "good-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "good-user"
|
||||
|
||||
@@ -161,24 +161,24 @@ async def test_login_flow_validates(data: hass_auth.Data, hass: HomeAssistant) -
|
||||
)
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "TEST-user ", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-USER", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "test-USER"
|
||||
|
||||
|
||||
@@ -260,24 +260,24 @@ async def test_legacy_login_flow_validates(
|
||||
)
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "test-user"
|
||||
|
||||
|
||||
|
||||
+15
-15
@@ -172,12 +172,12 @@ async def test_create_new_user(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
credential = step["result"]
|
||||
assert credential is not None
|
||||
|
||||
@@ -241,12 +241,12 @@ async def test_login_as_existing_user(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
credential = step["result"]
|
||||
user = await manager.async_get_user_by_credentials(credential)
|
||||
@@ -840,14 +840,14 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
# After auth_provider validated, request auth module input form
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
@@ -855,7 +855,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Invalid code error
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
assert step["errors"] == {"base": "invalid_code"}
|
||||
|
||||
@@ -864,7 +864,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Finally passed, get credential
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["result"]
|
||||
assert step["result"].id == "mock-id"
|
||||
|
||||
@@ -915,21 +915,21 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
# After auth_provider validated, request select auth module
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "select_mfa_module"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"multi_factor_auth_module": "module2"}
|
||||
)
|
||||
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
@@ -937,7 +937,7 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Finally passed, get credential
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["result"]
|
||||
assert step["result"].id == "mock-id"
|
||||
|
||||
@@ -983,13 +983,13 @@ async def test_auth_module_expired_session(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
with freeze_time(dt_util.utcnow() + MFA_SESSION_EXPIRATION):
|
||||
@@ -997,7 +997,7 @@ async def test_auth_module_expired_session(mock_hass) -> None:
|
||||
step["flow_id"], {"pin": "test-pin"}
|
||||
)
|
||||
# login flow abort due session timeout
|
||||
assert step["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert step["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert step["reason"] == "login_expired"
|
||||
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_reauth_flow_scenario(
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
assert flow["type"] == FlowResultType.FORM
|
||||
assert flow["type"] is FlowResultType.FORM
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
|
||||
fw_major = int(ap_status_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
||||
@@ -305,7 +305,7 @@ async def test_reauth_flow_scenarios(
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
assert flow["type"] == FlowResultType.FORM
|
||||
assert flow["type"] is FlowResultType.FORM
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
|
||||
with patch(
|
||||
@@ -337,7 +337,7 @@ async def test_reauth_flow_scenarios(
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
|
||||
@@ -284,7 +284,7 @@ async def test_setup_entry_failure(
|
||||
|
||||
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert result is False
|
||||
assert mock_config_entry.state == state
|
||||
assert mock_config_entry.state is state
|
||||
|
||||
|
||||
async def test_fetch_airos_data_auth_error(mock_airos_client: MagicMock) -> None:
|
||||
|
||||
@@ -138,4 +138,4 @@ async def test_migrate_future_version_returns_false(
|
||||
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.MIGRATION_ERROR
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
@@ -6,15 +6,18 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.analytics import LABS_SNAPSHOT_FEATURE
|
||||
from homeassistant.components.analytics import CONF_SNAPSHOTS_URL, LABS_SNAPSHOT_FEATURE
|
||||
from homeassistant.components.analytics.const import (
|
||||
BASIC_ENDPOINT_URL,
|
||||
BASIC_ENDPOINT_URL_DEV,
|
||||
DOMAIN,
|
||||
SNAPSHOT_DEFAULT_URL,
|
||||
SNAPSHOT_URL_PATH,
|
||||
STORAGE_KEY,
|
||||
)
|
||||
from homeassistant.components.hassio import HassioNotReadyError
|
||||
from homeassistant.components.labs import async_update_preview_feature
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -37,6 +40,175 @@ async def test_setup(hass: HomeAssistant) -> None:
|
||||
assert DOMAIN in hass.data
|
||||
|
||||
|
||||
async def test_setup_with_snapshots_url(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test setup with snapshots_url in YAML config sends snapshots to that URL."""
|
||||
custom_url = "https://custom-snapshot-endpoint.example.com"
|
||||
snapshot_endpoint = custom_url + SNAPSHOT_URL_PATH
|
||||
aioclient_mock.post(snapshot_endpoint, status=200, json={})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics._async_snapshot_payload",
|
||||
return_value={"mock": {}},
|
||||
):
|
||||
assert await async_setup_component(hass, "labs", {})
|
||||
assert await async_setup_component(
|
||||
hass, DOMAIN, {DOMAIN: {CONF_SNAPSHOTS_URL: custom_url}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "analytics/preferences", "preferences": {"snapshots": True}}
|
||||
)
|
||||
assert (await ws_client.receive_json())["success"]
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(str(call[1]) == snapshot_endpoint for call in aioclient_mock.mock_calls)
|
||||
|
||||
|
||||
async def test_setup_entry_supervisor_not_ready(hass: HomeAssistant) -> None:
|
||||
"""Test that HassioNotReadyError raises ConfigEntryNotReady."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_supervisor_info",
|
||||
side_effect=HassioNotReadyError,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_schedule_starts_and_sends_analytics(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that the analytics schedule fires and sends analytics after time travel."""
|
||||
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
|
||||
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, status=200)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION):
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "analytics/preferences", "preferences": {"base": True}}
|
||||
)
|
||||
assert (await ws_client.receive_json())["success"]
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=901))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that unloading the config entry stops the analytics schedule."""
|
||||
aioclient_mock.post(BASIC_ENDPOINT_URL, status=200)
|
||||
aioclient_mock.post(BASIC_ENDPOINT_URL_DEV, status=200)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION):
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "analytics/preferences", "preferences": {"base": True}}
|
||||
)
|
||||
await ws_client.receive_json()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=901))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_websocket_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test websocket returns error when analytics entry failed to load."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_supervisor_info",
|
||||
side_effect=HassioNotReadyError,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id({"type": "analytics"})
|
||||
response = await ws_client.receive_json()
|
||||
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "not_found"
|
||||
|
||||
|
||||
async def test_websocket_preferences_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test preferences websocket returns error when analytics entry failed to load."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_supervisor_info",
|
||||
side_effect=HassioNotReadyError,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "analytics/preferences", "preferences": {"base": True}}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "not_found"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_snapshot_payload")
|
||||
async def test_labs_feature_toggle(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -164,7 +164,7 @@ async def test_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ async def test_template_error(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ async def test_template_variables(
|
||||
hass, "hello", None, context, agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Okay, let me take care of that for you."
|
||||
@@ -382,7 +382,7 @@ async def test_function_call(
|
||||
system_text = " ".join(block["text"] for block in system if "text" in block)
|
||||
assert "You are a voice assistant for Home Assistant." in system_text
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "I have successfully called the function"
|
||||
@@ -457,7 +457,7 @@ async def test_function_exception(
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "There was an error calling the function"
|
||||
@@ -638,7 +638,7 @@ async def test_refusal(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -670,7 +670,7 @@ async def test_stream_wrong_type(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert result.response.speech["plain"]["speech"] == "Expected a stream of messages"
|
||||
|
||||
@@ -700,7 +700,7 @@ async def test_double_system_messages(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_auth_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@@ -86,7 +86,7 @@ async def test_connection_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
# Check new state
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_device_trigger_reauth_flow(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
mock_flow_init.assert_called_once()
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None:
|
||||
@@ -235,4 +235,4 @@ async def test_get_axis_api_errors(
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == state
|
||||
assert config_entry.state is state
|
||||
|
||||
@@ -26,7 +26,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@@ -34,7 +34,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
|
||||
BASE_CONFIG.copy(),
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert (
|
||||
result2["title"]
|
||||
== "cluster.region.kusto.windows.net / test-database-name (test-table-name)"
|
||||
@@ -61,7 +61,7 @@ async def test_config_flow_errors(
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data=None,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
# Test error handling with error
|
||||
@@ -71,7 +71,7 @@ async def test_config_flow_errors(
|
||||
result["flow_id"],
|
||||
BASE_CONFIG.copy(),
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": expected}
|
||||
|
||||
schema = result2["data_schema"]
|
||||
@@ -99,7 +99,7 @@ async def test_config_flow_errors(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
# Retest error handling if error is corrected and connection is successful
|
||||
|
||||
@@ -112,4 +112,4 @@ async def test_config_flow_errors(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result3["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_config_flow_step_user(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result1["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result1["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result1["result"].title == "Office occupied"
|
||||
assert result1["next_flow"][0] == FlowType.CONFIG_SUBENTRIES_FLOW
|
||||
|
||||
@@ -260,7 +260,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
entry_id = result["result"].entry_id
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -287,7 +287,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(entry_id)
|
||||
@@ -337,7 +337,7 @@ async def test_single_numeric_state_observation(hass: HomeAssistant) -> None:
|
||||
CONF_PRIOR: 20,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
await hass.async_block_till_done()
|
||||
@@ -408,7 +408,7 @@ async def test_multi_numeric_state_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -546,7 +546,7 @@ async def test_single_template_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -1086,7 +1086,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
assert result.get("errors") is None
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async def test_init_failure(
|
||||
"""Test an initialization error on integration load."""
|
||||
mock_bring_client.login.side_effect = exception
|
||||
await setup_integration(hass, bring_config_entry)
|
||||
assert bring_config_entry.state == status
|
||||
assert bring_config_entry.state is status
|
||||
|
||||
assert (
|
||||
any(
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_client_failure(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == expected_state
|
||||
assert config_entry.state is expected_state
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert [flow.get("step_id") for flow in flows] == expected_flows
|
||||
|
||||
@@ -56,7 +56,7 @@ async def test_async_unload_entry(
|
||||
result = await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
assert result is True
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
|
||||
@@ -201,7 +201,7 @@ async def test_set_temperature(
|
||||
{"temperature": {"value": 20}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
|
||||
# Select by area explicitly (climate_2)
|
||||
response = await intent.async_handle(
|
||||
@@ -211,7 +211,7 @@ async def test_set_temperature(
|
||||
{"area": {"value": bedroom_area.name}, "temperature": {"value": 20.1}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -228,7 +228,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -242,7 +242,7 @@ async def test_set_temperature(
|
||||
{"floor": {"value": second_floor.name}, "temperature": {"value": 20.3}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -259,7 +259,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -273,7 +273,7 @@ async def test_set_temperature(
|
||||
{"name": {"value": "Climate 2"}, "temperature": {"value": 20.5}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -291,7 +291,7 @@ async def test_set_temperature(
|
||||
|
||||
# Exception should contain details of what we tried to match
|
||||
assert isinstance(error.value, intent.MatchFailedError)
|
||||
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA
|
||||
assert error.value.result.no_match_reason is intent.MatchFailedReason.AREA
|
||||
constraints = error.value.constraints
|
||||
assert constraints.name is None
|
||||
assert constraints.area_name == office_area.name
|
||||
@@ -310,7 +310,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
|
||||
|
||||
async def test_set_temperature_no_entities(
|
||||
@@ -330,7 +330,7 @@ async def test_set_temperature_no_entities(
|
||||
{"temperature": {"value": 20}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.DOMAIN
|
||||
|
||||
|
||||
async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
|
||||
@@ -357,4 +357,4 @@ async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
|
||||
|
||||
# Exception should contain details of what we tried to match
|
||||
assert isinstance(error.value, intent.MatchFailedError)
|
||||
assert error.value.result.no_match_reason == intent.MatchFailedReason.FEATURE
|
||||
assert error.value.result.no_match_reason is intent.MatchFailedReason.FEATURE
|
||||
|
||||
@@ -114,4 +114,4 @@ async def test_migrate_future_version_returns_false(
|
||||
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.MIGRATION_ERROR
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
@@ -785,7 +785,7 @@ async def test_get_progress_index(
|
||||
)
|
||||
|
||||
for form in (form_hassio, form_user, form_reconfigure):
|
||||
assert form["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert form["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert form["step_id"] == "account"
|
||||
|
||||
await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"})
|
||||
@@ -961,9 +961,9 @@ async def test_get_progress_subscribe(
|
||||
"test", context=context
|
||||
)
|
||||
|
||||
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
for key in ("hassio", "user", "reauth", "reconfigure"):
|
||||
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["step_id"] == "account"
|
||||
|
||||
for key in ("hassio", "user", "reauth", "reconfigure"):
|
||||
@@ -1100,9 +1100,9 @@ async def test_get_progress_subscribe_in_progress(
|
||||
"test", context=context
|
||||
)
|
||||
|
||||
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
for key in ("hassio", "user", "reauth", "reconfigure"):
|
||||
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["step_id"] == "account"
|
||||
|
||||
await ws_client.send_json({"id": 1, "type": "config_entries/flow/subscribe"})
|
||||
@@ -1235,16 +1235,16 @@ async def test_get_progress_subscribe_in_progress_bad_flow(
|
||||
"test", context=context
|
||||
)
|
||||
|
||||
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
for key in ("hassio", "user", "reauth", "reconfigure"):
|
||||
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert forms[key]["step_id"] == "account"
|
||||
|
||||
with mock_config_flow("test2", BadFlow):
|
||||
forms["bad"] = await hass.config_entries.flow.async_init(
|
||||
"test2", context={"source": core_ce.SOURCE_REAUTH, "entry_id": "1234"}
|
||||
)
|
||||
assert forms["bad"]["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert forms["bad"]["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert forms["bad"]["step_id"] == "account"
|
||||
|
||||
await ws_client.send_json({"id": 1, "type": "config_entries/flow/subscribe"})
|
||||
|
||||
@@ -116,7 +116,7 @@ async def test_hidden_entities_skipped(
|
||||
)
|
||||
|
||||
assert len(calls) == 0
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
|
||||
|
||||
@@ -135,13 +135,13 @@ async def test_exposed_domains(hass: HomeAssistant) -> None:
|
||||
result = await conversation.async_converse(
|
||||
hass, "unlock front door", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "run my script", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ async def test_exposed_areas(
|
||||
)
|
||||
|
||||
# All is well for the exposed kitchen light
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
|
||||
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
|
||||
@@ -202,14 +202,14 @@ async def test_exposed_areas(
|
||||
)
|
||||
|
||||
# This should be an error because the lights in that area are not exposed
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
|
||||
# But we can still ask questions about the bedroom, even with no exposed entities
|
||||
result = await conversation.async_converse(
|
||||
hass, "how many lights are on in the bedroom?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
@@ -248,7 +248,7 @@ async def test_punctuation(hass: HomeAssistant) -> None:
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["entity_id"][0] == "light.test_light"
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["name"]["value"] == "test light"
|
||||
assert result.response.intent.slots["name"]["text"] == "test light"
|
||||
@@ -326,7 +326,7 @@ async def test_unexposed_entities_skipped(
|
||||
)
|
||||
|
||||
assert len(calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
|
||||
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
|
||||
@@ -338,7 +338,7 @@ async def test_unexposed_entities_skipped(
|
||||
hass, "how many lights are on in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(result.response.matched_states) == 1
|
||||
assert result.response.matched_states[0].entity_id == exposed_light.entity_id
|
||||
|
||||
@@ -403,7 +403,7 @@ async def test_duplicated_names_resolved_with_device_area(
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["entity_id"][0] == bedroom_light.entity_id
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots.get("name", {}).get("value") == name
|
||||
assert result.response.intent.slots.get("name", {}).get("text") == name
|
||||
@@ -421,7 +421,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
|
||||
unregister = manager.register_trigger(trigger_sentences, callback)
|
||||
|
||||
result = await conversation.async_converse(hass, "Not the trigger", None, Context())
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# Using different case and including punctuation
|
||||
test_sentences = ["it's party time!", "IT IS TIME TO PARTY."]
|
||||
@@ -430,7 +430,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
|
||||
result = await conversation.async_converse(hass, sentence, None, Context())
|
||||
assert callback.call_count == 1
|
||||
assert callback.call_args[0][0].text == sentence
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, (
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE, (
|
||||
sentence
|
||||
)
|
||||
assert result.response.speech == {
|
||||
@@ -443,7 +443,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
|
||||
callback.reset_mock()
|
||||
for sentence in test_sentences:
|
||||
result = await conversation.async_converse(hass, sentence, None, Context())
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR, (
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR, (
|
||||
sentence
|
||||
)
|
||||
|
||||
@@ -479,7 +479,7 @@ async def test_trigger_sentence_response_translation(
|
||||
result = await conversation.async_converse(
|
||||
hass, "test sentence", None, Context()
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.speech == {
|
||||
"plain": {"speech": expected, "extra_data": None}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ async def test_shopping_list_add_item(hass: HomeAssistant) -> None:
|
||||
result = await conversation.async_converse(
|
||||
hass, "add apples to my shopping list", None, Context()
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.speech == {
|
||||
"plain": {"speech": "Added apples", "extra_data": None}
|
||||
}
|
||||
@@ -506,7 +506,7 @@ async def test_nevermind_intent(hass: HomeAssistant) -> None:
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.intent_type == intent.INTENT_NEVERMIND
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert not result.response.speech
|
||||
|
||||
|
||||
@@ -517,7 +517,7 @@ async def test_respond_intent(hass: HomeAssistant) -> None:
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.intent_type == intent.INTENT_RESPOND
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.speech["plain"]["speech"] == "Hello from Home Assistant."
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ async def test_satellite_area_context(
|
||||
satellite_id=kitchen_satellite.entity_id,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
|
||||
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
|
||||
@@ -608,7 +608,7 @@ async def test_satellite_area_context(
|
||||
satellite_id=kitchen_satellite.entity_id,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
|
||||
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
|
||||
@@ -632,7 +632,7 @@ async def test_satellite_area_context(
|
||||
device_id=bedroom_satellite.id,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
|
||||
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
|
||||
@@ -652,7 +652,7 @@ async def test_satellite_area_context(
|
||||
hass, f"turn {command} all lights", None, Context(), None
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# All lights should have been targeted
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
@@ -667,7 +667,7 @@ async def test_error_no_device(hass: HomeAssistant) -> None:
|
||||
hass, "turn on missing entity", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -685,7 +685,7 @@ async def test_error_no_device_exposed(hass: HomeAssistant) -> None:
|
||||
hass, "turn on kitchen light", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -700,7 +700,7 @@ async def test_error_no_area(hass: HomeAssistant) -> None:
|
||||
hass, "turn on the lights in missing area", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -715,7 +715,7 @@ async def test_error_no_floor(hass: HomeAssistant) -> None:
|
||||
hass, "turn on all the lights on missing floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -734,7 +734,7 @@ async def test_error_no_device_in_area(
|
||||
hass, "turn on missing entity in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -754,7 +754,7 @@ async def test_error_no_device_on_floor(
|
||||
hass, "turn on missing entity on ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -809,7 +809,7 @@ async def test_error_no_device_on_floor_exposed(
|
||||
hass, "turn on test light on the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -848,7 +848,7 @@ async def test_error_no_device_in_area_exposed(
|
||||
hass, "turn on test light in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -877,7 +877,7 @@ async def test_error_no_domain(hass: HomeAssistant) -> None:
|
||||
hass, "turn on the fans", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -912,7 +912,7 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None:
|
||||
hass, "turn on the fans", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -931,7 +931,7 @@ async def test_error_no_domain_in_area(
|
||||
hass, "turn on the lights in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -967,7 +967,7 @@ async def test_error_no_domain_in_area_exposed(
|
||||
hass, "turn on the lights in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -991,7 +991,7 @@ async def test_error_no_domain_on_floor(
|
||||
hass, "turn on all lights on the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1009,7 +1009,7 @@ async def test_error_no_domain_on_floor(
|
||||
hass, "turn on all lights upstairs", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1048,7 +1048,7 @@ async def test_error_no_domain_on_floor_exposed(
|
||||
hass, "turn on all lights on the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1086,7 +1086,7 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None:
|
||||
hass, "open the windows", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1135,7 +1135,7 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None:
|
||||
hass, "open all the windows", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1156,7 +1156,7 @@ async def test_error_no_device_class_in_area(
|
||||
hass, "open bedroom windows", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1191,7 +1191,7 @@ async def test_error_no_device_class_in_area_exposed(
|
||||
hass, "open bedroom windows", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1246,7 +1246,7 @@ async def test_error_no_device_class_on_floor_exposed(
|
||||
hass, "open ground floor windows", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1268,7 +1268,7 @@ async def test_error_no_intent(hass: HomeAssistant) -> None:
|
||||
hass, "do something", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
|
||||
)
|
||||
@@ -1305,7 +1305,7 @@ async def test_error_duplicate_names(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name}", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1319,7 +1319,7 @@ async def test_error_duplicate_names(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"is {name} on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1362,7 +1362,7 @@ async def test_duplicate_names_but_one_is_exposed(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name}", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.matched_states[0].entity_id == kitchen_light_1.entity_id
|
||||
|
||||
|
||||
@@ -1399,7 +1399,7 @@ async def test_error_duplicate_names_same_area(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1414,7 +1414,7 @@ async def test_error_duplicate_names_same_area(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1464,7 +1464,7 @@ async def test_duplicate_names_same_area_but_one_is_exposed(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.matched_states[0].entity_id == kitchen_light_1.entity_id
|
||||
|
||||
|
||||
@@ -1530,20 +1530,20 @@ async def test_duplicate_names_different_areas(
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name}", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# Target kitchen light by using kitchen device
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name}", None, Context(), None, device_id=device_kitchen.id
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
|
||||
|
||||
# Target bedroom light by using bedroom device
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn on {name}", None, Context(), None, device_id=device_bedroom.id
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
|
||||
|
||||
|
||||
@@ -1562,7 +1562,7 @@ async def test_error_wrong_state(hass: HomeAssistant) -> None:
|
||||
hass, "pause test player", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert result.response.speech["plain"]["speech"] == "Sorry, no device is playing"
|
||||
|
||||
@@ -1583,7 +1583,7 @@ async def test_error_feature_not_supported(hass: HomeAssistant) -> None:
|
||||
hass, "set test player volume to 100%", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1615,7 +1615,7 @@ async def test_error_no_timer_support(
|
||||
hass, "set a 5 minute timer", None, Context(), None, device_id=device_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1639,7 +1639,7 @@ async def test_error_timer_not_found(hass: HomeAssistant) -> None:
|
||||
hass, "pause timer", None, Context(), None, device_id=device_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"] == "Sorry, I couldn't find that timer"
|
||||
@@ -1677,18 +1677,18 @@ async def test_error_multiple_timers_matched(
|
||||
result = await conversation.async_converse(
|
||||
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# Cannot target multiple timers
|
||||
result = await conversation.async_converse(
|
||||
hass, "cancel timer", None, Context(), None, device_id=device_id
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1714,7 +1714,7 @@ async def test_no_states_matched_default_error(
|
||||
hass, "turn on lights in the kitchen", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
@@ -1802,7 +1802,7 @@ async def test_all_domains_loaded(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
# Invalid target vs. no intent recognized
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -1856,7 +1856,7 @@ async def test_same_named_entities_in_different_areas(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert (
|
||||
result.response.intent.slots.get("name", {}).get("value") == kitchen_light.name
|
||||
@@ -1876,7 +1876,7 @@ async def test_same_named_entities_in_different_areas(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert (
|
||||
result.response.intent.slots.get("name", {}).get("value") == bedroom_light.name
|
||||
@@ -1892,19 +1892,19 @@ async def test_same_named_entities_in_different_areas(
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on overhead light", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# Querying a duplicate name should also fail
|
||||
result = await conversation.async_converse(
|
||||
hass, "is the overhead light on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# But we can still ask questions that don't rely on the name
|
||||
result = await conversation.async_converse(
|
||||
hass, "how many lights are on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
@@ -1955,7 +1955,7 @@ async def test_same_aliased_entities_in_different_areas(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
|
||||
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
|
||||
@@ -1971,7 +1971,7 @@ async def test_same_aliased_entities_in_different_areas(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.intent is not None
|
||||
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
|
||||
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
|
||||
@@ -1983,19 +1983,19 @@ async def test_same_aliased_entities_in_different_areas(
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on overhead light", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# Querying a duplicate alias should also fail
|
||||
result = await conversation.async_converse(
|
||||
hass, "is the overhead light on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# But we can still ask questions that don't rely on the alias
|
||||
result = await conversation.async_converse(
|
||||
hass, "how many lights are on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
@@ -2027,7 +2027,7 @@ async def test_device_id_in_handler(hass: HomeAssistant) -> None:
|
||||
Context(),
|
||||
device_id=device_id,
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert handler.device_id == device_id
|
||||
|
||||
|
||||
@@ -2070,7 +2070,7 @@ async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None:
|
||||
result = await conversation.async_converse(
|
||||
hass, "I'd like to order a stout please", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert beer_handler.triggered
|
||||
assert not food_handler.triggered
|
||||
|
||||
@@ -2079,7 +2079,7 @@ async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None:
|
||||
result = await conversation.async_converse(
|
||||
hass, "I'd like to order a cookie please", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert not beer_handler.triggered
|
||||
assert food_handler.triggered
|
||||
|
||||
@@ -2871,7 +2871,7 @@ async def test_query_same_name_different_areas(
|
||||
result = await conversation.async_converse(
|
||||
hass, "is the overhead light on?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
# Succeeds using area from device (kitchen)
|
||||
result = await conversation.async_converse(
|
||||
@@ -2882,7 +2882,7 @@ async def test_query_same_name_different_areas(
|
||||
None,
|
||||
device_id=kitchen_device.id,
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(result.response.matched_states) == 1
|
||||
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
|
||||
|
||||
@@ -3059,7 +3059,7 @@ async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# Not exposed
|
||||
expose_entity(hass, "light.test_light", False)
|
||||
@@ -3072,7 +3072,7 @@ async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -3339,7 +3339,7 @@ async def test_state_names_are_not_translated(
|
||||
result = await conversation.async_converse(
|
||||
hass, "what is the weather like?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
|
||||
mock_async_render.assert_called_once()
|
||||
|
||||
assert (
|
||||
@@ -3396,7 +3396,7 @@ async def test_intent_tool_call_in_chat_log(hass: HomeAssistant) -> None:
|
||||
hass, "turn on test light", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
|
||||
@@ -3448,7 +3448,7 @@ async def test_trigger_tool_call_in_chat_log(hass: HomeAssistant) -> None:
|
||||
hass, trigger_sentence, None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
|
||||
@@ -3486,7 +3486,7 @@ async def test_no_tool_call_on_no_intent_match(hass: HomeAssistant) -> None:
|
||||
hass, "this is a random sentence that should not match", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
|
||||
@@ -3511,7 +3511,7 @@ async def test_intent_tool_call_with_error_response(hass: HomeAssistant) -> None
|
||||
hass, "turn on the non existent device", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
|
||||
with (
|
||||
|
||||
@@ -88,7 +88,7 @@ async def test_cover_set_position(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Opening"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -102,7 +102,7 @@ async def test_cover_set_position(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Closing"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -116,7 +116,7 @@ async def test_cover_set_position(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Position set"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -144,7 +144,7 @@ async def test_cover_device_class(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Opening the garage"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -168,7 +168,7 @@ async def test_valve_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Opening"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -182,7 +182,7 @@ async def test_valve_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Closing"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -196,7 +196,7 @@ async def test_valve_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Position set"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -229,7 +229,7 @@ async def test_vacuum_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Started"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -243,7 +243,7 @@ async def test_vacuum_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Returning"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -275,7 +275,7 @@ async def test_media_player_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Paused"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -294,7 +294,7 @@ async def test_media_player_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Resumed"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -313,7 +313,7 @@ async def test_media_player_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Playing next"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -329,7 +329,7 @@ async def test_media_player_intents(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Volume set"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
@@ -399,7 +399,7 @@ async def test_turn_floor_lights_on_off(
|
||||
)
|
||||
|
||||
assert len(on_calls) == 2
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
kitchen_light.entity_id,
|
||||
living_room_light.entity_id,
|
||||
@@ -411,7 +411,7 @@ async def test_turn_floor_lights_on_off(
|
||||
)
|
||||
|
||||
assert len(on_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
@@ -422,7 +422,7 @@ async def test_turn_floor_lights_on_off(
|
||||
)
|
||||
|
||||
assert len(off_calls) == 1
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
bedroom_light.entity_id
|
||||
}
|
||||
@@ -474,7 +474,7 @@ async def test_date_time(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "September 17th, 2013"
|
||||
|
||||
result = await conversation.async_converse(
|
||||
@@ -483,5 +483,5 @@ async def test_date_time(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "1:02 AM"
|
||||
|
||||
@@ -57,7 +57,7 @@ async def test_init_failure(
|
||||
"""Test an initialization error on integration load."""
|
||||
mock_cookidoo_client.login.side_effect = exception
|
||||
await setup_integration(hass, cookidoo_config_entry)
|
||||
assert cookidoo_config_entry.state == status
|
||||
assert cookidoo_config_entry.state is status
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user