mirror of
https://github.com/home-assistant/core.git
synced 2025-09-09 14:51:34 +02:00
Merge branch 'dev_target_triggers_conditions' of github.com:home-assistant/core into target_trigger
This commit is contained in:
10
.github/workflows/builder.yml
vendored
10
.github/workflows/builder.yml
vendored
@@ -190,7 +190,7 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -330,14 +330,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -502,7 +502,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@v1.2.4
|
uses: actions/ai-inference@v1.2.7
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@v1.2.4
|
uses: actions/ai-inference@v1.2.7
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
@@ -120,6 +120,9 @@ class AuthStore:
|
|||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
|
while new_user.id in self._users:
|
||||||
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
self._users[new_user.id] = new_user
|
self._users[new_user.id] = new_user
|
||||||
|
|
||||||
if credentials is None:
|
if credentials is None:
|
||||||
|
@@ -6,11 +6,11 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
ConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
ConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
DataMissingError,
|
AirOSDataMissingError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
KeyDataMissingError,
|
AirOSKeyDataMissingError,
|
||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
airos_data = await airos_device.status()
|
airos_data = await airos_device.status()
|
||||||
|
|
||||||
except (
|
except (
|
||||||
ConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
):
|
):
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (ConnectionAuthenticationError, DataMissingError):
|
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except KeyDataMissingError:
|
except AirOSKeyDataMissingError:
|
||||||
errors["base"] = "key_data_missing"
|
errors["base"] = "key_data_missing"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
@@ -6,10 +6,10 @@ import logging
|
|||||||
|
|
||||||
from airos.airos8 import AirOS, AirOSData
|
from airos.airos8 import AirOS, AirOSData
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
ConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
ConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
DataMissingError,
|
AirOSDataMissingError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
|||||||
try:
|
try:
|
||||||
await self.airos_device.login()
|
await self.airos_device.login()
|
||||||
return await self.airos_device.status()
|
return await self.airos_device.status()
|
||||||
except (ConnectionAuthenticationError,) as err:
|
except (AirOSConnectionAuthenticationError,) as err:
|
||||||
_LOGGER.exception("Error authenticating with airOS device")
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
) from err
|
) from err
|
||||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
except (
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
TimeoutError,
|
||||||
|
) as err:
|
||||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="cannot_connect",
|
translation_key="cannot_connect",
|
||||||
) from err
|
) from err
|
||||||
except (DataMissingError,) as err:
|
except (AirOSDataMissingError,) as err:
|
||||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.2.1"]
|
"requirements": ["airos==0.2.4"]
|
||||||
}
|
}
|
||||||
|
@@ -69,13 +69,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
translation_key="wireless_essid",
|
translation_key="wireless_essid",
|
||||||
value_fn=lambda data: data.wireless.essid,
|
value_fn=lambda data: data.wireless.essid,
|
||||||
),
|
),
|
||||||
AirOSSensorEntityDescription(
|
|
||||||
key="wireless_mode",
|
|
||||||
translation_key="wireless_mode",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
|
||||||
options=WIRELESS_MODE_OPTIONS,
|
|
||||||
),
|
|
||||||
AirOSSensorEntityDescription(
|
AirOSSensorEntityDescription(
|
||||||
key="wireless_antenna_gain",
|
key="wireless_antenna_gain",
|
||||||
translation_key="wireless_antenna_gain",
|
translation_key="wireless_antenna_gain",
|
||||||
|
@@ -43,13 +43,6 @@
|
|||||||
"wireless_essid": {
|
"wireless_essid": {
|
||||||
"name": "Wireless SSID"
|
"name": "Wireless SSID"
|
||||||
},
|
},
|
||||||
"wireless_mode": {
|
|
||||||
"name": "Wireless mode",
|
|
||||||
"state": {
|
|
||||||
"ap_ptp": "Access point",
|
|
||||||
"sta_ptp": "Station"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wireless_antenna_gain": {
|
"wireless_antenna_gain": {
|
||||||
"name": "Antenna gain"
|
"name": "Antenna gain"
|
||||||
},
|
},
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["caldav", "vobject"],
|
"loggers": ["caldav", "vobject"],
|
||||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==0.110.1"],
|
"requirements": ["hass-nabucasa==0.111.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# If path is relative, we assume relative to Home Assistant config dir
|
# If path is relative, we assume relative to Home Assistant config dir
|
||||||
if not os.path.isabs(download_path):
|
if not os.path.isabs(download_path):
|
||||||
download_path = hass.config.path(download_path)
|
download_path = hass.config.path(download_path)
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
|
||||||
|
)
|
||||||
|
|
||||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@@ -11,6 +11,7 @@ import requests
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.service import async_register_admin_service
|
from homeassistant.helpers.service import async_register_admin_service
|
||||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||||
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
|
|||||||
|
|
||||||
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||||
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
||||||
|
url: str = service.data[ATTR_URL]
|
||||||
def do_download() -> None:
|
subdir: str | None = service.data.get(ATTR_SUBDIR)
|
||||||
"""Download the file."""
|
target_filename: str | None = service.data.get(ATTR_FILENAME)
|
||||||
try:
|
overwrite: bool = service.data[ATTR_OVERWRITE]
|
||||||
url = service.data[ATTR_URL]
|
|
||||||
|
|
||||||
subdir = service.data.get(ATTR_SUBDIR)
|
|
||||||
|
|
||||||
filename = service.data.get(ATTR_FILENAME)
|
|
||||||
|
|
||||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
|
||||||
|
|
||||||
if subdir:
|
if subdir:
|
||||||
# Check the path
|
# Check the path
|
||||||
|
try:
|
||||||
raise_if_invalid_path(subdir)
|
raise_if_invalid_path(subdir)
|
||||||
|
except ValueError as err:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="subdir_invalid",
|
||||||
|
translation_placeholders={"subdir": subdir},
|
||||||
|
) from err
|
||||||
|
if os.path.isabs(subdir):
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="subdir_not_relative",
|
||||||
|
translation_placeholders={"subdir": subdir},
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_download() -> None:
|
||||||
|
"""Download the file."""
|
||||||
final_path = None
|
final_path = None
|
||||||
|
filename = target_filename
|
||||||
|
try:
|
||||||
req = requests.get(url, stream=True, timeout=10)
|
req = requests.get(url, stream=True, timeout=10)
|
||||||
|
|
||||||
if req.status_code != HTTPStatus.OK:
|
if req.status_code != HTTPStatus.OK:
|
||||||
|
@@ -12,6 +12,14 @@
|
|||||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"subdir_invalid": {
|
||||||
|
"message": "Invalid subdirectory, got: {subdir}"
|
||||||
|
},
|
||||||
|
"subdir_not_relative": {
|
||||||
|
"message": "Subdirectory must be relative, got: {subdir}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"download_file": {
|
"download_file": {
|
||||||
"name": "Download file",
|
"name": "Download file",
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
|
||||||
}
|
}
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250731.0"]
|
"requirements": ["home-assistant-frontend==20250805.0"]
|
||||||
}
|
}
|
||||||
|
@@ -123,10 +123,10 @@
|
|||||||
},
|
},
|
||||||
"ai_task_data": {
|
"ai_task_data": {
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add Generate data with AI service",
|
"user": "Add AI task",
|
||||||
"reconfigure": "Reconfigure Generate data with AI service"
|
"reconfigure": "Reconfigure AI task"
|
||||||
},
|
},
|
||||||
"entry_type": "Generate data with AI service",
|
"entry_type": "AI task",
|
||||||
"step": {
|
"step": {
|
||||||
"set_options": {
|
"set_options": {
|
||||||
"data": {
|
"data": {
|
||||||
|
@@ -226,6 +226,10 @@
|
|||||||
"unsupported_virtualization_image": {
|
"unsupported_virtualization_image": {
|
||||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
||||||
|
},
|
||||||
|
"unsupported_os_version": {
|
||||||
|
"title": "Unsupported system - Home Assistant OS version",
|
||||||
|
"description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
@@ -21,6 +21,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_reset_cutting_blade_usage_time(
|
||||||
|
session: AutomowerSession,
|
||||||
|
mower_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Reset cutting blade usage time."""
|
||||||
|
await session.commands.reset_cutting_blade_usage_time(mower_id)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_cutting_blade_usage_time_availability(data: MowerAttributes) -> bool:
|
||||||
|
"""Return True if blade usage time is greater than 0."""
|
||||||
|
value = data.statistics.cutting_blade_usage_time
|
||||||
|
return value is not None and value > 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||||
"""Describes Automower button entities."""
|
"""Describes Automower button entities."""
|
||||||
@@ -28,6 +42,7 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
|||||||
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||||
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||||
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
|
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
|
||||||
|
poll_after_sending: bool = False
|
||||||
|
|
||||||
|
|
||||||
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
||||||
@@ -43,6 +58,14 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
|||||||
translation_key="sync_clock",
|
translation_key="sync_clock",
|
||||||
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
|
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
|
||||||
),
|
),
|
||||||
|
AutomowerButtonEntityDescription(
|
||||||
|
key="reset_cutting_blade_usage_time",
|
||||||
|
translation_key="reset_cutting_blade_usage_time",
|
||||||
|
available_fn=reset_cutting_blade_usage_time_availability,
|
||||||
|
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
|
||||||
|
press_fn=async_reset_cutting_blade_usage_time,
|
||||||
|
poll_after_sending=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,3 +116,5 @@ class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity):
|
|||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Send a command to the mower."""
|
"""Send a command to the mower."""
|
||||||
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
|
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
|
||||||
|
if self.entity_description.poll_after_sending:
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ from aioautomower.exceptions import (
|
|||||||
HusqvarnaTimeoutError,
|
HusqvarnaTimeoutError,
|
||||||
HusqvarnaWSServerHandshakeError,
|
HusqvarnaWSServerHandshakeError,
|
||||||
)
|
)
|
||||||
from aioautomower.model import MowerDictionary
|
from aioautomower.model import MowerDictionary, MowerStates
|
||||||
from aioautomower.session import AutomowerSession
|
from aioautomower.session import AutomowerSession
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -29,7 +29,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
MAX_WS_RECONNECT_TIME = 600
|
MAX_WS_RECONNECT_TIME = 600
|
||||||
SCAN_INTERVAL = timedelta(minutes=8)
|
SCAN_INTERVAL = timedelta(minutes=8)
|
||||||
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
||||||
|
PONG_TIMEOUT = timedelta(seconds=90)
|
||||||
|
PING_INTERVAL = timedelta(seconds=10)
|
||||||
|
PING_TIMEOUT = timedelta(seconds=5)
|
||||||
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
|
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +60,9 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
|
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
|
||||||
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
|
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
|
||||||
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
|
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
|
||||||
|
self.pong: datetime | None = None
|
||||||
|
self.websocket_alive: bool = False
|
||||||
|
self._watchdog_task: asyncio.Task | None = None
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@callback
|
@callback
|
||||||
@@ -71,6 +76,18 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
await self.api.connect()
|
await self.api.connect()
|
||||||
self.api.register_data_callback(self.handle_websocket_updates)
|
self.api.register_data_callback(self.handle_websocket_updates)
|
||||||
self.ws_connected = True
|
self.ws_connected = True
|
||||||
|
|
||||||
|
def start_watchdog() -> None:
|
||||||
|
if self._watchdog_task is not None and not self._watchdog_task.done():
|
||||||
|
_LOGGER.debug("Cancelling previous watchdog task")
|
||||||
|
self._watchdog_task.cancel()
|
||||||
|
self._watchdog_task = self.config_entry.async_create_background_task(
|
||||||
|
self.hass,
|
||||||
|
self._pong_watchdog(),
|
||||||
|
"websocket_watchdog",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api.register_ws_ready_callback(start_watchdog)
|
||||||
try:
|
try:
|
||||||
data = await self.api.get_status()
|
data = await self.api.get_status()
|
||||||
except ApiError as err:
|
except ApiError as err:
|
||||||
@@ -93,6 +110,19 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
mower_data.capabilities.work_areas for mower_data in self.data.values()
|
mower_data.capabilities.work_areas for mower_data in self.data.values()
|
||||||
):
|
):
|
||||||
self._async_add_remove_work_areas()
|
self._async_add_remove_work_areas()
|
||||||
|
if (
|
||||||
|
not self._should_poll()
|
||||||
|
and self.update_interval is not None
|
||||||
|
and self.websocket_alive
|
||||||
|
):
|
||||||
|
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
|
||||||
|
self.update_interval = None
|
||||||
|
if self.update_interval is None and self._should_poll():
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Polling re-enabled via WebSocket: at least one mower active"
|
||||||
|
)
|
||||||
|
self.update_interval = SCAN_INTERVAL
|
||||||
|
self.hass.async_create_task(self.async_request_refresh())
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
|
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
|
||||||
@@ -161,6 +191,30 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
"reconnect_task",
|
"reconnect_task",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _should_poll(self) -> bool:
|
||||||
|
"""Return True if at least one mower is connected and at least one is not OFF."""
|
||||||
|
return any(mower.metadata.connected for mower in self.data.values()) and any(
|
||||||
|
mower.mower.state != MowerStates.OFF for mower in self.data.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _pong_watchdog(self) -> None:
|
||||||
|
_LOGGER.debug("Watchdog started")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
_LOGGER.debug("Sending ping")
|
||||||
|
self.websocket_alive = await self.api.send_empty_message()
|
||||||
|
_LOGGER.debug("Ping result: %s", self.websocket_alive)
|
||||||
|
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
|
||||||
|
if not self.websocket_alive:
|
||||||
|
_LOGGER.debug("No pong received → restart polling")
|
||||||
|
if self.update_interval is None:
|
||||||
|
self.update_interval = SCAN_INTERVAL
|
||||||
|
await self.async_request_refresh()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
_LOGGER.debug("Watchdog cancelled")
|
||||||
|
|
||||||
def _async_add_remove_devices(self) -> None:
|
def _async_add_remove_devices(self) -> None:
|
||||||
"""Add new devices and remove orphaned devices from the registry."""
|
"""Add new devices and remove orphaned devices from the registry."""
|
||||||
current_devices = set(self.data)
|
current_devices = set(self.data)
|
||||||
|
@@ -8,6 +8,9 @@
|
|||||||
"button": {
|
"button": {
|
||||||
"sync_clock": {
|
"sync_clock": {
|
||||||
"default": "mdi:clock-check-outline"
|
"default": "mdi:clock-check-outline"
|
||||||
|
},
|
||||||
|
"reset_cutting_blade_usage_time": {
|
||||||
|
"default": "mdi:saw-blade"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioautomower"],
|
"loggers": ["aioautomower"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioautomower==2.1.1"]
|
"requirements": ["aioautomower==2.1.2"]
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,9 @@
|
|||||||
},
|
},
|
||||||
"sync_clock": {
|
"sync_clock": {
|
||||||
"name": "Sync clock"
|
"name": "Sync clock"
|
||||||
|
},
|
||||||
|
"reset_cutting_blade_usage_time": {
|
||||||
|
"name": "Reset cutting blade usage time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"xknx==3.8.0",
|
"xknx==3.8.0",
|
||||||
"xknxproject==3.8.2",
|
"xknxproject==3.8.2",
|
||||||
"knx-frontend==2025.7.23.50952"
|
"knx-frontend==2025.8.4.154919"
|
||||||
],
|
],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -285,7 +285,9 @@ DISCOVERY_SCHEMAS = [
|
|||||||
native_min_value=0.5,
|
native_min_value=0.5,
|
||||||
native_step=0.5,
|
native_step=0.5,
|
||||||
device_to_ha=(
|
device_to_ha=(
|
||||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
lambda x: None
|
||||||
|
if x is None
|
||||||
|
else min(x, 200) / 2 # Matter range (1-200, capped at 200)
|
||||||
),
|
),
|
||||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||||
mode=NumberMode.SLIDER,
|
mode=NumberMode.SLIDER,
|
||||||
|
@@ -130,6 +130,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
list_id=self._shopping_list_id,
|
list_id=self._shopping_list_id,
|
||||||
note=item.summary.strip() if item.summary else item.summary,
|
note=item.summary.strip() if item.summary else item.summary,
|
||||||
position=position,
|
position=position,
|
||||||
|
quantity=0.0,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await self.coordinator.client.add_shopping_item(new_shopping_item)
|
await self.coordinator.client.add_shopping_item(new_shopping_item)
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"mqtt": {
|
"_": {
|
||||||
"trigger": "mdi:swap-horizontal"
|
"trigger": "mdi:swap-horizontal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1285,7 +1285,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"mqtt": {
|
"_": {
|
||||||
"name": "MQTT",
|
"name": "MQTT",
|
||||||
"description": "When a specific message is received on a given MQTT topic.",
|
"description": "When a specific message is received on a given MQTT topic.",
|
||||||
"description_configured": "When an MQTT message has been received",
|
"description_configured": "When an MQTT message has been received",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Describes the format for MQTT triggers
|
# Describes the format for MQTT triggers
|
||||||
|
|
||||||
mqtt:
|
_:
|
||||||
fields:
|
fields:
|
||||||
payload:
|
payload:
|
||||||
example: "on"
|
example: "on"
|
||||||
|
@@ -58,10 +58,10 @@
|
|||||||
},
|
},
|
||||||
"ai_task_data": {
|
"ai_task_data": {
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add Generate data with AI service",
|
"user": "Add AI task",
|
||||||
"reconfigure": "Reconfigure Generate data with AI service"
|
"reconfigure": "Reconfigure AI task"
|
||||||
},
|
},
|
||||||
"entry_type": "Generate data with AI service",
|
"entry_type": "AI task",
|
||||||
"step": {
|
"step": {
|
||||||
"set_options": {
|
"set_options": {
|
||||||
"data": {
|
"data": {
|
||||||
|
@@ -52,9 +52,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add Generate data with AI service"
|
"user": "Add AI task"
|
||||||
},
|
},
|
||||||
"entry_type": "Generate data with AI service",
|
"entry_type": "AI task",
|
||||||
"abort": {
|
"abort": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
@@ -73,10 +73,10 @@
|
|||||||
},
|
},
|
||||||
"ai_task_data": {
|
"ai_task_data": {
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add Generate data with AI service",
|
"user": "Add AI task",
|
||||||
"reconfigure": "Reconfigure Generate data with AI service"
|
"reconfigure": "Reconfigure AI task"
|
||||||
},
|
},
|
||||||
"entry_type": "Generate data with AI service",
|
"entry_type": "AI task",
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
|
@@ -422,9 +422,7 @@ class ReolinkVODMediaSource(MediaSource):
|
|||||||
file_name = f"{file.start_time.time()} {file.duration}"
|
file_name = f"{file.start_time.time()} {file.duration}"
|
||||||
if file.triggers != file.triggers.NONE:
|
if file.triggers != file.triggers.NONE:
|
||||||
file_name += " " + " ".join(
|
file_name += " " + " ".join(
|
||||||
str(trigger.name).title()
|
str(trigger.name).title() for trigger in file.triggers
|
||||||
for trigger in file.triggers
|
|
||||||
if trigger != trigger.NONE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
children.append(
|
children.append(
|
||||||
|
@@ -116,6 +116,7 @@ NUMBER_ENTITIES = (
|
|||||||
cmd_id=[289, 438],
|
cmd_id=[289, 438],
|
||||||
translation_key="floodlight_brightness",
|
translation_key="floodlight_brightness",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
native_max_value=100,
|
native_max_value=100,
|
||||||
@@ -407,8 +408,8 @@ NUMBER_ENTITIES = (
|
|||||||
key="auto_track_limit_left",
|
key="auto_track_limit_left",
|
||||||
cmd_key="GetPtzTraceSection",
|
cmd_key="GetPtzTraceSection",
|
||||||
translation_key="auto_track_limit_left",
|
translation_key="auto_track_limit_left",
|
||||||
mode=NumberMode.SLIDER,
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_min_value=-1,
|
native_min_value=-1,
|
||||||
native_max_value=2700,
|
native_max_value=2700,
|
||||||
@@ -420,8 +421,8 @@ NUMBER_ENTITIES = (
|
|||||||
key="auto_track_limit_right",
|
key="auto_track_limit_right",
|
||||||
cmd_key="GetPtzTraceSection",
|
cmd_key="GetPtzTraceSection",
|
||||||
translation_key="auto_track_limit_right",
|
translation_key="auto_track_limit_right",
|
||||||
mode=NumberMode.SLIDER,
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_min_value=-1,
|
native_min_value=-1,
|
||||||
native_max_value=2700,
|
native_max_value=2700,
|
||||||
@@ -435,6 +436,7 @@ NUMBER_ENTITIES = (
|
|||||||
translation_key="auto_track_disappear_time",
|
translation_key="auto_track_disappear_time",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
@@ -451,6 +453,7 @@ NUMBER_ENTITIES = (
|
|||||||
translation_key="auto_track_stop_time",
|
translation_key="auto_track_stop_time",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
|
@@ -440,6 +440,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_battery_level": {
|
||||||
|
"title": "Deprecated battery level option in {entity_name}",
|
||||||
|
"description": "The template vacuum options `battery_level` and `battery_level_template` are being removed in 2026.8.\n\nPlease remove the `battery_level` or `battery_level_template` option from the YAML configuration for {entity_id} ({entity_name})."
|
||||||
|
}
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"alarm_control_panel": {
|
"alarm_control_panel": {
|
||||||
|
@@ -34,11 +34,16 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import config_validation as cv, template
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
issue_registry as ir,
|
||||||
|
template,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import (
|
||||||
AddConfigEntryEntitiesCallback,
|
AddConfigEntryEntitiesCallback,
|
||||||
AddEntitiesCallback,
|
AddEntitiesCallback,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -188,6 +193,26 @@ def async_create_preview_vacuum(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_issue(
|
||||||
|
hass: HomeAssistant, supported_features: int, name: str, entity_id: str
|
||||||
|
) -> None:
|
||||||
|
"""Create the battery_level issue."""
|
||||||
|
if supported_features & VacuumEntityFeature.BATTERY:
|
||||||
|
key = "deprecated_battery_level"
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"{key}_{entity_id}",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key=key,
|
||||||
|
translation_placeholders={
|
||||||
|
"entity_name": name,
|
||||||
|
"entity_id": entity_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||||
"""Representation of a template vacuum features."""
|
"""Representation of a template vacuum features."""
|
||||||
|
|
||||||
@@ -369,6 +394,16 @@ class TemplateStateVacuumEntity(TemplateEntity, AbstractTemplateVacuum):
|
|||||||
self.add_script(action_id, action_config, name, DOMAIN)
|
self.add_script(action_id, action_config, name, DOMAIN)
|
||||||
self._attr_supported_features |= supported_feature
|
self._attr_supported_features |= supported_feature
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when entity about to be added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
create_issue(
|
||||||
|
self.hass,
|
||||||
|
self._attr_supported_features,
|
||||||
|
self._attr_name or DEFAULT_NAME,
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_setup_templates(self) -> None:
|
def _async_setup_templates(self) -> None:
|
||||||
"""Set up templates."""
|
"""Set up templates."""
|
||||||
@@ -434,6 +469,16 @@ class TriggerVacuumEntity(TriggerEntity, AbstractTemplateVacuum):
|
|||||||
self._to_render_simple.append(key)
|
self._to_render_simple.append(key)
|
||||||
self._parse_result.add(key)
|
self._parse_result.add(key)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when entity about to be added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
create_issue(
|
||||||
|
self.hass,
|
||||||
|
self._attr_supported_features,
|
||||||
|
self._attr_name or DEFAULT_NAME,
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle update of the data."""
|
"""Handle update of the data."""
|
||||||
|
@@ -314,6 +314,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
|||||||
),
|
),
|
||||||
TAMPER_BINARY_SENSOR,
|
TAMPER_BINARY_SENSOR,
|
||||||
),
|
),
|
||||||
|
# Zigbee gateway
|
||||||
|
# Undocumented
|
||||||
|
"wg2": (
|
||||||
|
TuyaBinarySensorEntityDescription(
|
||||||
|
key=DPCode.MASTER_STATE,
|
||||||
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
on_value="alarm",
|
||||||
|
),
|
||||||
|
),
|
||||||
# Thermostatic Radiator Valve
|
# Thermostatic Radiator Valve
|
||||||
# Not documented
|
# Not documented
|
||||||
"wkf": (
|
"wkf": (
|
||||||
|
@@ -109,6 +109,7 @@ class DPCode(StrEnum):
|
|||||||
ANION = "anion" # Ionizer unit
|
ANION = "anion" # Ionizer unit
|
||||||
ARM_DOWN_PERCENT = "arm_down_percent"
|
ARM_DOWN_PERCENT = "arm_down_percent"
|
||||||
ARM_UP_PERCENT = "arm_up_percent"
|
ARM_UP_PERCENT = "arm_up_percent"
|
||||||
|
ATMOSPHERIC_PRESSTURE = "atmospheric_pressture" # Typo is in Tuya API
|
||||||
BASIC_ANTI_FLICKER = "basic_anti_flicker"
|
BASIC_ANTI_FLICKER = "basic_anti_flicker"
|
||||||
BASIC_DEVICE_VOLUME = "basic_device_volume"
|
BASIC_DEVICE_VOLUME = "basic_device_volume"
|
||||||
BASIC_FLIP = "basic_flip"
|
BASIC_FLIP = "basic_flip"
|
||||||
@@ -215,6 +216,10 @@ class DPCode(StrEnum):
|
|||||||
HUMIDITY = "humidity" # Humidity
|
HUMIDITY = "humidity" # Humidity
|
||||||
HUMIDITY_CURRENT = "humidity_current" # Current humidity
|
HUMIDITY_CURRENT = "humidity_current" # Current humidity
|
||||||
HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity
|
HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity
|
||||||
|
HUMIDITY_OUTDOOR = "humidity_outdoor" # Outdoor humidity
|
||||||
|
HUMIDITY_OUTDOOR_1 = "humidity_outdoor_1" # Outdoor humidity
|
||||||
|
HUMIDITY_OUTDOOR_2 = "humidity_outdoor_2" # Outdoor humidity
|
||||||
|
HUMIDITY_OUTDOOR_3 = "humidity_outdoor_3" # Outdoor humidity
|
||||||
HUMIDITY_SET = "humidity_set" # Humidity setting
|
HUMIDITY_SET = "humidity_set" # Humidity setting
|
||||||
HUMIDITY_VALUE = "humidity_value" # Humidity
|
HUMIDITY_VALUE = "humidity_value" # Humidity
|
||||||
IPC_WORK_MODE = "ipc_work_mode"
|
IPC_WORK_MODE = "ipc_work_mode"
|
||||||
@@ -360,6 +365,15 @@ class DPCode(StrEnum):
|
|||||||
TEMP_CURRENT_EXTERNAL = (
|
TEMP_CURRENT_EXTERNAL = (
|
||||||
"temp_current_external" # Current external temperature in Celsius
|
"temp_current_external" # Current external temperature in Celsius
|
||||||
)
|
)
|
||||||
|
TEMP_CURRENT_EXTERNAL_1 = (
|
||||||
|
"temp_current_external_1" # Current external temperature in Celsius
|
||||||
|
)
|
||||||
|
TEMP_CURRENT_EXTERNAL_2 = (
|
||||||
|
"temp_current_external_2" # Current external temperature in Celsius
|
||||||
|
)
|
||||||
|
TEMP_CURRENT_EXTERNAL_3 = (
|
||||||
|
"temp_current_external_3" # Current external temperature in Celsius
|
||||||
|
)
|
||||||
TEMP_CURRENT_EXTERNAL_F = (
|
TEMP_CURRENT_EXTERNAL_F = (
|
||||||
"temp_current_external_f" # Current external temperature in Fahrenheit
|
"temp_current_external_f" # Current external temperature in Fahrenheit
|
||||||
)
|
)
|
||||||
@@ -405,6 +419,7 @@ class DPCode(StrEnum):
|
|||||||
WINDOW_CHECK = "window_check"
|
WINDOW_CHECK = "window_check"
|
||||||
WINDOW_STATE = "window_state"
|
WINDOW_STATE = "window_state"
|
||||||
WINDSPEED = "windspeed"
|
WINDSPEED = "windspeed"
|
||||||
|
WINDSPEED_AVG = "windspeed_avg"
|
||||||
WIRELESS_BATTERYLOCK = "wireless_batterylock"
|
WIRELESS_BATTERYLOCK = "wireless_batterylock"
|
||||||
WIRELESS_ELECTRICITY = "wireless_electricity"
|
WIRELESS_ELECTRICITY = "wireless_electricity"
|
||||||
WORK_MODE = "work_mode" # Working mode
|
WORK_MODE = "work_mode" # Working mode
|
||||||
|
@@ -267,7 +267,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
|||||||
return int(self._speed.remap_value_to(value, 1, 100))
|
return int(self._speed.remap_value_to(value, 1, 100))
|
||||||
|
|
||||||
if self._speeds is not None:
|
if self._speeds is not None:
|
||||||
if (value := self.device.status.get(self._speeds.dpcode)) is None:
|
if (
|
||||||
|
value := self.device.status.get(self._speeds.dpcode)
|
||||||
|
) is None or value not in self._speeds.range:
|
||||||
return None
|
return None
|
||||||
return ordered_list_item_to_percentage(self._speeds.range, value)
|
return ordered_list_item_to_percentage(self._speeds.range, value)
|
||||||
|
|
||||||
|
@@ -846,6 +846,27 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.TEMP_CURRENT_EXTERNAL_1,
|
||||||
|
translation_key="indexed_temperature_external",
|
||||||
|
translation_placeholders={"index": "1"},
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.TEMP_CURRENT_EXTERNAL_2,
|
||||||
|
translation_key="indexed_temperature_external",
|
||||||
|
translation_placeholders={"index": "2"},
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.TEMP_CURRENT_EXTERNAL_3,
|
||||||
|
translation_key="indexed_temperature_external",
|
||||||
|
translation_placeholders={"index": "3"},
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
TuyaSensorEntityDescription(
|
TuyaSensorEntityDescription(
|
||||||
key=DPCode.VA_HUMIDITY,
|
key=DPCode.VA_HUMIDITY,
|
||||||
translation_key="humidity",
|
translation_key="humidity",
|
||||||
@@ -858,12 +879,51 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
|||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.HUMIDITY_OUTDOOR,
|
||||||
|
translation_key="humidity_outdoor",
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.HUMIDITY_OUTDOOR_1,
|
||||||
|
translation_key="indexed_humidity_outdoor",
|
||||||
|
translation_placeholders={"index": "1"},
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.HUMIDITY_OUTDOOR_2,
|
||||||
|
translation_key="indexed_humidity_outdoor",
|
||||||
|
translation_placeholders={"index": "2"},
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.HUMIDITY_OUTDOOR_3,
|
||||||
|
translation_key="indexed_humidity_outdoor",
|
||||||
|
translation_placeholders={"index": "3"},
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.ATMOSPHERIC_PRESSTURE,
|
||||||
|
translation_key="air_pressure",
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
TuyaSensorEntityDescription(
|
TuyaSensorEntityDescription(
|
||||||
key=DPCode.BRIGHT_VALUE,
|
key=DPCode.BRIGHT_VALUE,
|
||||||
translation_key="illuminance",
|
translation_key="illuminance",
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.WINDSPEED_AVG,
|
||||||
|
translation_key="wind_speed",
|
||||||
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
*BATTERY_SENSORS,
|
*BATTERY_SENSORS,
|
||||||
),
|
),
|
||||||
# Gas Detector
|
# Gas Detector
|
||||||
|
@@ -502,9 +502,21 @@
|
|||||||
"temperature_external": {
|
"temperature_external": {
|
||||||
"name": "Probe temperature"
|
"name": "Probe temperature"
|
||||||
},
|
},
|
||||||
|
"indexed_temperature_external": {
|
||||||
|
"name": "Probe temperature channel {index}"
|
||||||
|
},
|
||||||
"humidity": {
|
"humidity": {
|
||||||
"name": "[%key:component::sensor::entity_component::humidity::name%]"
|
"name": "[%key:component::sensor::entity_component::humidity::name%]"
|
||||||
},
|
},
|
||||||
|
"humidity_outdoor": {
|
||||||
|
"name": "Outdoor humidity"
|
||||||
|
},
|
||||||
|
"indexed_humidity_outdoor": {
|
||||||
|
"name": "Outdoor humidity channel {index}"
|
||||||
|
},
|
||||||
|
"air_pressure": {
|
||||||
|
"name": "Air pressure"
|
||||||
|
},
|
||||||
"pm25": {
|
"pm25": {
|
||||||
"name": "[%key:component::sensor::entity_component::pm25::name%]"
|
"name": "[%key:component::sensor::entity_component::pm25::name%]"
|
||||||
},
|
},
|
||||||
|
@@ -79,6 +79,8 @@ DEFAULT_NAME = "Vacuum cleaner robot"
|
|||||||
_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
|
_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
|
||||||
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
|
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
|
||||||
|
|
||||||
|
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
|
||||||
|
|
||||||
|
|
||||||
class VacuumEntityFeature(IntFlag):
|
class VacuumEntityFeature(IntFlag):
|
||||||
"""Supported features of the vacuum entity."""
|
"""Supported features of the vacuum entity."""
|
||||||
@@ -321,7 +323,11 @@ class StateVacuumEntity(
|
|||||||
|
|
||||||
Integrations should implement a sensor instead.
|
Integrations should implement a sensor instead.
|
||||||
"""
|
"""
|
||||||
if self.platform:
|
if (
|
||||||
|
self.platform
|
||||||
|
and self.platform.platform_name
|
||||||
|
not in _BATTERY_DEPRECATION_IGNORED_PLATFORMS
|
||||||
|
):
|
||||||
# Don't report usage until after entity added to hass, after init
|
# Don't report usage until after entity added to hass, after init
|
||||||
report_usage(
|
report_usage(
|
||||||
f"is setting the {property} which has been deprecated."
|
f"is setting the {property} which has been deprecated."
|
||||||
@@ -341,7 +347,11 @@ class StateVacuumEntity(
|
|||||||
Integrations should remove the battery supported feature when migrating
|
Integrations should remove the battery supported feature when migrating
|
||||||
battery level and icon to a sensor.
|
battery level and icon to a sensor.
|
||||||
"""
|
"""
|
||||||
if self.platform:
|
if (
|
||||||
|
self.platform
|
||||||
|
and self.platform.platform_name
|
||||||
|
not in _BATTERY_DEPRECATION_IGNORED_PLATFORMS
|
||||||
|
):
|
||||||
# Don't report usage until after entity added to hass, after init
|
# Don't report usage until after entity added to hass, after init
|
||||||
report_usage(
|
report_usage(
|
||||||
f"is setting the battery supported feature which has been deprecated."
|
f"is setting the battery supported feature which has been deprecated."
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/wyoming",
|
"documentation": "https://www.home-assistant.io/integrations/wyoming",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["wyoming==1.7.1"],
|
"requirements": ["wyoming==1.7.2"],
|
||||||
"zeroconf": ["_wyoming._tcp.local."]
|
"zeroconf": ["_wyoming._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -509,7 +509,7 @@ class ControllerEvents:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.async_check_preprovisioned_device(node)
|
await self.async_check_pre_provisioned_device(node)
|
||||||
|
|
||||||
if node.is_controller_node:
|
if node.is_controller_node:
|
||||||
# Create a controller status sensor for each device
|
# Create a controller status sensor for each device
|
||||||
@@ -637,8 +637,8 @@ class ControllerEvents:
|
|||||||
f"{DOMAIN}.identify_controller.{dev_id[1]}",
|
f"{DOMAIN}.identify_controller.{dev_id[1]}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None:
|
async def async_check_pre_provisioned_device(self, node: ZwaveNode) -> None:
|
||||||
"""Check if the node was preprovisioned and update the device registry."""
|
"""Check if the node was pre-provisioned and update the device registry."""
|
||||||
provisioning_entry = (
|
provisioning_entry = (
|
||||||
await self.driver_events.driver.controller.async_get_provisioning_entry(
|
await self.driver_events.driver.controller.async_get_provisioning_entry(
|
||||||
node.node_id
|
node.node_id
|
||||||
@@ -648,27 +648,35 @@ class ControllerEvents:
|
|||||||
provisioning_entry
|
provisioning_entry
|
||||||
and provisioning_entry.additional_properties
|
and provisioning_entry.additional_properties
|
||||||
and "device_id" in provisioning_entry.additional_properties
|
and "device_id" in provisioning_entry.additional_properties
|
||||||
):
|
and (
|
||||||
preprovisioned_device = self.dev_reg.async_get(
|
pre_provisioned_device := self.dev_reg.async_get(
|
||||||
provisioning_entry.additional_properties["device_id"]
|
provisioning_entry.additional_properties["device_id"]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if preprovisioned_device:
|
and (dsk_identifier := (DOMAIN, f"provision_{provisioning_entry.dsk}"))
|
||||||
dsk = provisioning_entry.dsk
|
in pre_provisioned_device.identifiers
|
||||||
dsk_identifier = (DOMAIN, f"provision_{dsk}")
|
):
|
||||||
|
|
||||||
# If the pre-provisioned device has the DSK identifier, remove it
|
|
||||||
if dsk_identifier in preprovisioned_device.identifiers:
|
|
||||||
driver = self.driver_events.driver
|
driver = self.driver_events.driver
|
||||||
device_id = get_device_id(driver, node)
|
device_id = get_device_id(driver, node)
|
||||||
device_id_ext = get_device_id_ext(driver, node)
|
device_id_ext = get_device_id_ext(driver, node)
|
||||||
new_identifiers = preprovisioned_device.identifiers.copy()
|
new_identifiers = pre_provisioned_device.identifiers.copy()
|
||||||
new_identifiers.remove(dsk_identifier)
|
new_identifiers.remove(dsk_identifier)
|
||||||
new_identifiers.add(device_id)
|
new_identifiers.add(device_id)
|
||||||
if device_id_ext:
|
if device_id_ext:
|
||||||
new_identifiers.add(device_id_ext)
|
new_identifiers.add(device_id_ext)
|
||||||
|
|
||||||
|
if self.dev_reg.async_get_device(identifiers=new_identifiers):
|
||||||
|
# If a device entry is registered with the node ID based identifiers,
|
||||||
|
# just remove the device entry with the DSK identifier.
|
||||||
self.dev_reg.async_update_device(
|
self.dev_reg.async_update_device(
|
||||||
preprovisioned_device.id,
|
pre_provisioned_device.id,
|
||||||
|
remove_config_entry_id=self.config_entry.entry_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add the node ID based identifiers to the device entry
|
||||||
|
# with the DSK identifier and remove the DSK identifier.
|
||||||
|
self.dev_reg.async_update_device(
|
||||||
|
pre_provisioned_device.id,
|
||||||
new_identifiers=new_identifiers,
|
new_identifiers=new_identifiers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -8,8 +8,8 @@ from homeassistant.helpers.trigger import Trigger
|
|||||||
from .triggers import event, value_updated
|
from .triggers import event, value_updated
|
||||||
|
|
||||||
TRIGGERS = {
|
TRIGGERS = {
|
||||||
event.PLATFORM_TYPE: event.EventTrigger,
|
event.RELATIVE_PLATFORM_TYPE: event.EventTrigger,
|
||||||
value_updated.PLATFORM_TYPE: value_updated.ValueUpdatedTrigger,
|
value_updated.RELATIVE_PLATFORM_TYPE: value_updated.ValueUpdatedTrigger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -34,8 +34,11 @@ from ..helpers import (
|
|||||||
)
|
)
|
||||||
from .trigger_helpers import async_bypass_dynamic_config_validation
|
from .trigger_helpers import async_bypass_dynamic_config_validation
|
||||||
|
|
||||||
|
# Relative platform type should be <SUBMODULE_NAME>
|
||||||
|
RELATIVE_PLATFORM_TYPE = f"{__name__.rsplit('.', maxsplit=1)[-1]}"
|
||||||
|
|
||||||
# Platform type should be <DOMAIN>.<SUBMODULE_NAME>
|
# Platform type should be <DOMAIN>.<SUBMODULE_NAME>
|
||||||
PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}"
|
PLATFORM_TYPE = f"{DOMAIN}.{RELATIVE_PLATFORM_TYPE}"
|
||||||
|
|
||||||
|
|
||||||
def validate_non_node_event_source(obj: dict) -> dict:
|
def validate_non_node_event_source(obj: dict) -> dict:
|
||||||
|
@@ -37,8 +37,11 @@ from ..const import (
|
|||||||
from ..helpers import async_get_nodes_from_targets, get_device_id
|
from ..helpers import async_get_nodes_from_targets, get_device_id
|
||||||
from .trigger_helpers import async_bypass_dynamic_config_validation
|
from .trigger_helpers import async_bypass_dynamic_config_validation
|
||||||
|
|
||||||
|
# Relative platform type should be <SUBMODULE_NAME>
|
||||||
|
RELATIVE_PLATFORM_TYPE = f"{__name__.rsplit('.', maxsplit=1)[-1]}"
|
||||||
|
|
||||||
# Platform type should be <DOMAIN>.<SUBMODULE_NAME>
|
# Platform type should be <DOMAIN>.<SUBMODULE_NAME>
|
||||||
PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}"
|
PLATFORM_TYPE = f"{DOMAIN}.{RELATIVE_PLATFORM_TYPE}"
|
||||||
|
|
||||||
ATTR_FROM = "from"
|
ATTR_FROM = "from"
|
||||||
ATTR_TO = "to"
|
ATTR_TO = "to"
|
||||||
|
21
homeassistant/helpers/automation.py
Normal file
21
homeassistant/helpers/automation.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Helpers for automation."""
|
||||||
|
|
||||||
|
|
||||||
|
def get_absolute_description_key(domain: str, key: str) -> str:
|
||||||
|
"""Return the absolute description key."""
|
||||||
|
if not key.startswith("_"):
|
||||||
|
return f"{domain}.{key}"
|
||||||
|
key = key[1:] # Remove leading underscore
|
||||||
|
if not key:
|
||||||
|
return domain
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def get_relative_description_key(domain: str, key: str) -> str:
|
||||||
|
"""Return the relative description key."""
|
||||||
|
platform, *subtype = key.split(".", 1)
|
||||||
|
if platform != domain:
|
||||||
|
return f"_{key}"
|
||||||
|
if not subtype:
|
||||||
|
return "_"
|
||||||
|
return subtype[0]
|
@@ -644,6 +644,13 @@ def slug(value: Any) -> str:
|
|||||||
raise vol.Invalid(f"invalid slug {value} (try {slg})")
|
raise vol.Invalid(f"invalid slug {value} (try {slg})")
|
||||||
|
|
||||||
|
|
||||||
|
def underscore_slug(value: Any) -> str:
|
||||||
|
"""Validate value is a valid slug, possibly starting with an underscore."""
|
||||||
|
if value.startswith("_"):
|
||||||
|
return f"_{slug(value[1:])}"
|
||||||
|
return slug(value)
|
||||||
|
|
||||||
|
|
||||||
def schema_with_slug_keys(
|
def schema_with_slug_keys(
|
||||||
value_schema: dict | Callable, *, slug_validator: Callable[[Any], str] = slug
|
value_schema: dict | Callable, *, slug_validator: Callable[[Any], str] = slug
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
|
@@ -40,9 +40,9 @@ from homeassistant.loader import (
|
|||||||
from homeassistant.util.async_ import create_eager_task
|
from homeassistant.util.async_ import create_eager_task
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
from homeassistant.util.yaml import load_yaml_dict
|
from homeassistant.util.yaml import load_yaml_dict
|
||||||
from homeassistant.util.yaml.loader import JSON_TYPE
|
|
||||||
|
|
||||||
from . import config_validation as cv, selector
|
from . import config_validation as cv, selector
|
||||||
|
from .automation import get_absolute_description_key, get_relative_description_key
|
||||||
from .integration_platform import async_process_integration_platforms
|
from .integration_platform import async_process_integration_platforms
|
||||||
from .selector import TargetSelector
|
from .selector import TargetSelector
|
||||||
from .template import Template
|
from .template import Template
|
||||||
@@ -100,7 +100,7 @@ def starts_with_dot(key: str) -> str:
|
|||||||
_TRIGGERS_SCHEMA = vol.Schema(
|
_TRIGGERS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Remove(vol.All(str, starts_with_dot)): object,
|
vol.Remove(vol.All(str, starts_with_dot)): object,
|
||||||
cv.slug: vol.Any(None, _TRIGGER_SCHEMA),
|
cv.underscore_slug: vol.Any(None, _TRIGGER_SCHEMA),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,6 +139,7 @@ async def _register_trigger_platform(
|
|||||||
|
|
||||||
if hasattr(platform, "async_get_triggers"):
|
if hasattr(platform, "async_get_triggers"):
|
||||||
for trigger_key in await platform.async_get_triggers(hass):
|
for trigger_key in await platform.async_get_triggers(hass):
|
||||||
|
trigger_key = get_absolute_description_key(integration_domain, trigger_key)
|
||||||
hass.data[TRIGGERS][trigger_key] = integration_domain
|
hass.data[TRIGGERS][trigger_key] = integration_domain
|
||||||
new_triggers.add(trigger_key)
|
new_triggers.add(trigger_key)
|
||||||
elif hasattr(platform, "async_validate_trigger_config") or hasattr(
|
elif hasattr(platform, "async_validate_trigger_config") or hasattr(
|
||||||
@@ -357,9 +358,8 @@ class PluggableAction:
|
|||||||
|
|
||||||
|
|
||||||
async def _async_get_trigger_platform(
|
async def _async_get_trigger_platform(
|
||||||
hass: HomeAssistant, config: ConfigType
|
hass: HomeAssistant, trigger_key: str
|
||||||
) -> TriggerProtocol:
|
) -> tuple[str, TriggerProtocol]:
|
||||||
trigger_key: str = config[CONF_PLATFORM]
|
|
||||||
platform_and_sub_type = trigger_key.split(".")
|
platform_and_sub_type = trigger_key.split(".")
|
||||||
platform = platform_and_sub_type[0]
|
platform = platform_and_sub_type[0]
|
||||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||||
@@ -368,7 +368,7 @@ async def _async_get_trigger_platform(
|
|||||||
except IntegrationNotFound:
|
except IntegrationNotFound:
|
||||||
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified") from None
|
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified") from None
|
||||||
try:
|
try:
|
||||||
return await integration.async_get_platform("trigger")
|
return platform, await integration.async_get_platform("trigger")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise vol.Invalid(
|
raise vol.Invalid(
|
||||||
f"Integration '{platform}' does not provide trigger support"
|
f"Integration '{platform}' does not provide trigger support"
|
||||||
@@ -381,11 +381,14 @@ async def async_validate_trigger_config(
|
|||||||
"""Validate triggers."""
|
"""Validate triggers."""
|
||||||
config = []
|
config = []
|
||||||
for conf in trigger_config:
|
for conf in trigger_config:
|
||||||
platform = await _async_get_trigger_platform(hass, conf)
|
trigger_key: str = conf[CONF_PLATFORM]
|
||||||
|
platform_domain, platform = await _async_get_trigger_platform(hass, trigger_key)
|
||||||
if hasattr(platform, "async_get_triggers"):
|
if hasattr(platform, "async_get_triggers"):
|
||||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||||
trigger_key: str = conf[CONF_PLATFORM]
|
relative_trigger_key = get_relative_description_key(
|
||||||
if not (trigger := trigger_descriptors.get(trigger_key)):
|
platform_domain, trigger_key
|
||||||
|
)
|
||||||
|
if not (trigger := trigger_descriptors.get(relative_trigger_key)):
|
||||||
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified")
|
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified")
|
||||||
conf = await trigger.async_validate_trigger_config(hass, conf)
|
conf = await trigger.async_validate_trigger_config(hass, conf)
|
||||||
elif hasattr(platform, "async_validate_trigger_config"):
|
elif hasattr(platform, "async_validate_trigger_config"):
|
||||||
@@ -471,7 +474,8 @@ async def async_initialize_triggers(
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
platform = await _async_get_trigger_platform(hass, conf)
|
trigger_key: str = conf[CONF_PLATFORM]
|
||||||
|
platform_domain, platform = await _async_get_trigger_platform(hass, trigger_key)
|
||||||
trigger_id = conf.get(CONF_ID, f"{idx}")
|
trigger_id = conf.get(CONF_ID, f"{idx}")
|
||||||
trigger_idx = f"{idx}"
|
trigger_idx = f"{idx}"
|
||||||
trigger_alias = conf.get(CONF_ALIAS)
|
trigger_alias = conf.get(CONF_ALIAS)
|
||||||
@@ -487,7 +491,10 @@ async def async_initialize_triggers(
|
|||||||
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
||||||
if hasattr(platform, "async_get_triggers"):
|
if hasattr(platform, "async_get_triggers"):
|
||||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||||
trigger = trigger_descriptors[conf[CONF_PLATFORM]](hass, conf)
|
relative_trigger_key = get_relative_description_key(
|
||||||
|
platform_domain, trigger_key
|
||||||
|
)
|
||||||
|
trigger = trigger_descriptors[relative_trigger_key](hass, conf)
|
||||||
coro = trigger.async_attach_trigger(action_wrapper, info)
|
coro = trigger.async_attach_trigger(action_wrapper, info)
|
||||||
else:
|
else:
|
||||||
coro = platform.async_attach_trigger(hass, conf, action_wrapper, info)
|
coro = platform.async_attach_trigger(hass, conf, action_wrapper, info)
|
||||||
@@ -525,11 +532,11 @@ async def async_initialize_triggers(
|
|||||||
return remove_triggers
|
return remove_triggers
|
||||||
|
|
||||||
|
|
||||||
def _load_triggers_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
|
def _load_triggers_file(integration: Integration) -> dict[str, Any]:
|
||||||
"""Load triggers file for an integration."""
|
"""Load triggers file for an integration."""
|
||||||
try:
|
try:
|
||||||
return cast(
|
return cast(
|
||||||
JSON_TYPE,
|
dict[str, Any],
|
||||||
_TRIGGERS_SCHEMA(
|
_TRIGGERS_SCHEMA(
|
||||||
load_yaml_dict(str(integration.file_path / "triggers.yaml"))
|
load_yaml_dict(str(integration.file_path / "triggers.yaml"))
|
||||||
),
|
),
|
||||||
@@ -549,11 +556,14 @@ def _load_triggers_file(hass: HomeAssistant, integration: Integration) -> JSON_T
|
|||||||
|
|
||||||
|
|
||||||
def _load_triggers_files(
|
def _load_triggers_files(
|
||||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
integrations: Iterable[Integration],
|
||||||
) -> dict[str, JSON_TYPE]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""Load trigger files for multiple integrations."""
|
"""Load trigger files for multiple integrations."""
|
||||||
return {
|
return {
|
||||||
integration.domain: _load_triggers_file(hass, integration)
|
integration.domain: {
|
||||||
|
get_absolute_description_key(integration.domain, key): value
|
||||||
|
for key, value in _load_triggers_file(integration).items()
|
||||||
|
}
|
||||||
for integration in integrations
|
for integration in integrations
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +584,7 @@ async def async_get_all_descriptions(
|
|||||||
return descriptions_cache
|
return descriptions_cache
|
||||||
|
|
||||||
# Files we loaded for missing descriptions
|
# Files we loaded for missing descriptions
|
||||||
new_triggers_descriptions: dict[str, JSON_TYPE] = {}
|
new_triggers_descriptions: dict[str, dict[str, Any]] = {}
|
||||||
# We try to avoid making a copy in the event the cache is good,
|
# We try to avoid making a copy in the event the cache is good,
|
||||||
# but now we must make a copy in case new triggers get added
|
# but now we must make a copy in case new triggers get added
|
||||||
# while we are loading the missing ones so we do not
|
# while we are loading the missing ones so we do not
|
||||||
@@ -601,7 +611,7 @@ async def async_get_all_descriptions(
|
|||||||
|
|
||||||
if integrations:
|
if integrations:
|
||||||
new_triggers_descriptions = await hass.async_add_executor_job(
|
new_triggers_descriptions = await hass.async_add_executor_job(
|
||||||
_load_triggers_files, hass, integrations
|
_load_triggers_files, integrations
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make a copy of the old cache and add missing descriptions to it
|
# Make a copy of the old cache and add missing descriptions to it
|
||||||
@@ -610,7 +620,7 @@ async def async_get_all_descriptions(
|
|||||||
domain = triggers[missing_trigger]
|
domain = triggers[missing_trigger]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
yaml_description := new_triggers_descriptions.get(domain, {}).get( # type: ignore[union-attr]
|
yaml_description := new_triggers_descriptions.get(domain, {}).get(
|
||||||
missing_trigger
|
missing_trigger
|
||||||
)
|
)
|
||||||
) is None:
|
) is None:
|
||||||
|
@@ -35,10 +35,10 @@ fnv-hash-fast==1.5.0
|
|||||||
go2rtc-client==0.2.1
|
go2rtc-client==0.2.1
|
||||||
ha-ffmpeg==3.2.2
|
ha-ffmpeg==3.2.2
|
||||||
habluetooth==4.0.1
|
habluetooth==4.0.1
|
||||||
hass-nabucasa==0.110.1
|
hass-nabucasa==0.111.0
|
||||||
hassil==2.2.3
|
hassil==2.2.3
|
||||||
home-assistant-bluetooth==1.13.1
|
home-assistant-bluetooth==1.13.1
|
||||||
home-assistant-frontend==20250731.0
|
home-assistant-frontend==20250805.0
|
||||||
home-assistant-intents==2025.7.30
|
home-assistant-intents==2025.7.30
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@@ -47,7 +47,7 @@ dependencies = [
|
|||||||
"fnv-hash-fast==1.5.0",
|
"fnv-hash-fast==1.5.0",
|
||||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||||
# integration
|
# integration
|
||||||
"hass-nabucasa==0.110.1",
|
"hass-nabucasa==0.111.0",
|
||||||
# When bumping httpx, please check the version pins of
|
# When bumping httpx, please check the version pins of
|
||||||
# httpcore, anyio, and h11 in gen_requirements_all
|
# httpcore, anyio, and h11 in gen_requirements_all
|
||||||
"httpx==0.28.1",
|
"httpx==0.28.1",
|
||||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@@ -22,7 +22,7 @@ certifi>=2021.5.30
|
|||||||
ciso8601==2.3.2
|
ciso8601==2.3.2
|
||||||
cronsim==2.6
|
cronsim==2.6
|
||||||
fnv-hash-fast==1.5.0
|
fnv-hash-fast==1.5.0
|
||||||
hass-nabucasa==0.110.1
|
hass-nabucasa==0.111.0
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
home-assistant-bluetooth==1.13.1
|
home-assistant-bluetooth==1.13.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
16
requirements_all.txt
generated
16
requirements_all.txt
generated
@@ -204,7 +204,7 @@ aioaseko==1.0.0
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2.1.1
|
aioautomower==2.1.2
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==2.2.1
|
aioazuredevops==2.2.1
|
||||||
@@ -453,7 +453,7 @@ airgradient==0.9.2
|
|||||||
airly==1.1.0
|
airly==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.airos
|
# homeassistant.components.airos
|
||||||
airos==0.2.1
|
airos==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.airthings_ble
|
# homeassistant.components.airthings_ble
|
||||||
airthings-ble==0.9.2
|
airthings-ble==0.9.2
|
||||||
@@ -777,7 +777,7 @@ decora-wifi==1.4
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==13.5.0
|
deebot-client==13.6.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@@ -1133,7 +1133,7 @@ habiticalib==0.4.1
|
|||||||
habluetooth==4.0.1
|
habluetooth==4.0.1
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.110.1
|
hass-nabucasa==0.111.0
|
||||||
|
|
||||||
# homeassistant.components.splunk
|
# homeassistant.components.splunk
|
||||||
hass-splunk==0.1.1
|
hass-splunk==0.1.1
|
||||||
@@ -1174,7 +1174,7 @@ hole==0.9.0
|
|||||||
holidays==0.77
|
holidays==0.77
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250731.0
|
home-assistant-frontend==20250805.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.7.30
|
home-assistant-intents==2025.7.30
|
||||||
@@ -1216,7 +1216,7 @@ ibmiotf==0.3.4
|
|||||||
ical==11.0.0
|
ical==11.0.0
|
||||||
|
|
||||||
# homeassistant.components.caldav
|
# homeassistant.components.caldav
|
||||||
icalendar==6.1.0
|
icalendar==6.3.1
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
@@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1
|
|||||||
knocki==0.4.2
|
knocki==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
knx-frontend==2025.7.23.50952
|
knx-frontend==2025.8.4.154919
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
@@ -3133,7 +3133,7 @@ wolf-comm==0.0.23
|
|||||||
wsdot==0.0.1
|
wsdot==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.wyoming
|
# homeassistant.components.wyoming
|
||||||
wyoming==1.7.1
|
wyoming==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.xbox
|
# homeassistant.components.xbox
|
||||||
xbox-webapi==2.1.0
|
xbox-webapi==2.1.0
|
||||||
|
@@ -13,7 +13,7 @@ freezegun==1.5.2
|
|||||||
go2rtc-client==0.2.1
|
go2rtc-client==0.2.1
|
||||||
license-expression==30.4.3
|
license-expression==30.4.3
|
||||||
mock-open==1.4.0
|
mock-open==1.4.0
|
||||||
mypy-dev==1.18.0a3
|
mypy-dev==1.18.0a4
|
||||||
pre-commit==4.2.0
|
pre-commit==4.2.0
|
||||||
pydantic==2.11.7
|
pydantic==2.11.7
|
||||||
pylint==3.3.7
|
pylint==3.3.7
|
||||||
|
16
requirements_test_all.txt
generated
16
requirements_test_all.txt
generated
@@ -192,7 +192,7 @@ aioaseko==1.0.0
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2.1.1
|
aioautomower==2.1.2
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==2.2.1
|
aioazuredevops==2.2.1
|
||||||
@@ -435,7 +435,7 @@ airgradient==0.9.2
|
|||||||
airly==1.1.0
|
airly==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.airos
|
# homeassistant.components.airos
|
||||||
airos==0.2.1
|
airos==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.airthings_ble
|
# homeassistant.components.airthings_ble
|
||||||
airthings-ble==0.9.2
|
airthings-ble==0.9.2
|
||||||
@@ -677,7 +677,7 @@ debugpy==1.8.14
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==13.5.0
|
deebot-client==13.6.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@@ -994,7 +994,7 @@ habiticalib==0.4.1
|
|||||||
habluetooth==4.0.1
|
habluetooth==4.0.1
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.110.1
|
hass-nabucasa==0.111.0
|
||||||
|
|
||||||
# homeassistant.components.assist_satellite
|
# homeassistant.components.assist_satellite
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
@@ -1023,7 +1023,7 @@ hole==0.9.0
|
|||||||
holidays==0.77
|
holidays==0.77
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250731.0
|
home-assistant-frontend==20250805.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.7.30
|
home-assistant-intents==2025.7.30
|
||||||
@@ -1056,7 +1056,7 @@ ibeacon-ble==1.2.0
|
|||||||
ical==11.0.0
|
ical==11.0.0
|
||||||
|
|
||||||
# homeassistant.components.caldav
|
# homeassistant.components.caldav
|
||||||
icalendar==6.1.0
|
icalendar==6.3.1
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
@@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0
|
|||||||
knocki==0.4.2
|
knocki==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
knx-frontend==2025.7.23.50952
|
knx-frontend==2025.8.4.154919
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
@@ -2586,7 +2586,7 @@ wolf-comm==0.0.23
|
|||||||
wsdot==0.0.1
|
wsdot==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.wyoming
|
# homeassistant.components.wyoming
|
||||||
wyoming==1.7.1
|
wyoming==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.xbox
|
# homeassistant.components.xbox
|
||||||
xbox-webapi==2.1.0
|
xbox-webapi==2.1.0
|
||||||
|
@@ -136,7 +136,7 @@ TRIGGER_ICONS_SCHEMA = cv.schema_with_slug_keys(
|
|||||||
vol.Optional("trigger"): icon_value_validator,
|
vol.Optional("trigger"): icon_value_validator,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
slug_validator=translation_key_validator,
|
slug_validator=cv.underscore_slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -450,7 +450,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
|
|||||||
slug_validator=translation_key_validator,
|
slug_validator=translation_key_validator,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
slug_validator=translation_key_validator,
|
slug_validator=cv.underscore_slug,
|
||||||
),
|
),
|
||||||
vol.Optional("conversation"): {
|
vol.Optional("conversation"): {
|
||||||
vol.Required("agent"): {
|
vol.Required("agent"): {
|
||||||
|
@@ -50,7 +50,7 @@ TRIGGER_SCHEMA = vol.Any(
|
|||||||
TRIGGERS_SCHEMA = vol.Schema(
|
TRIGGERS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Remove(vol.All(str, trigger.starts_with_dot)): object,
|
vol.Remove(vol.All(str, trigger.starts_with_dot)): object,
|
||||||
cv.slug: TRIGGER_SCHEMA,
|
cv.underscore_slug: TRIGGER_SCHEMA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
@@ -300,6 +300,20 @@ async def test_loading_does_not_write_right_away(
|
|||||||
assert hass_storage[auth_store.STORAGE_KEY] != {}
|
assert hass_storage[auth_store.STORAGE_KEY] != {}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_uuid(
|
||||||
|
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Test we don't override user if we have a duplicate user ID."""
|
||||||
|
hass_storage[auth_store.STORAGE_KEY] = MOCK_STORAGE_DATA
|
||||||
|
store = auth_store.AuthStore(hass)
|
||||||
|
await store.async_load()
|
||||||
|
with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex_mock:
|
||||||
|
hex_mock.side_effect = ["user-id", "new-id"]
|
||||||
|
user = await store.async_create_user("Test User")
|
||||||
|
assert len(hex_mock.mock_calls) == 2
|
||||||
|
assert user.id == "new-id"
|
||||||
|
|
||||||
|
|
||||||
async def test_add_remove_user_affects_tokens(
|
async def test_add_remove_user_affects_tokens(
|
||||||
hass: HomeAssistant, hass_storage: dict[str, Any]
|
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@@ -439,64 +439,6 @@
|
|||||||
'state': '5500',
|
'state': '5500',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-entry]
|
|
||||||
EntityRegistryEntrySnapshot({
|
|
||||||
'aliases': set({
|
|
||||||
}),
|
|
||||||
'area_id': None,
|
|
||||||
'capabilities': dict({
|
|
||||||
'options': list([
|
|
||||||
'ap_ptp',
|
|
||||||
'sta_ptp',
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
'config_entry_id': <ANY>,
|
|
||||||
'config_subentry_id': <ANY>,
|
|
||||||
'device_class': None,
|
|
||||||
'device_id': <ANY>,
|
|
||||||
'disabled_by': None,
|
|
||||||
'domain': 'sensor',
|
|
||||||
'entity_category': None,
|
|
||||||
'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode',
|
|
||||||
'has_entity_name': True,
|
|
||||||
'hidden_by': None,
|
|
||||||
'icon': None,
|
|
||||||
'id': <ANY>,
|
|
||||||
'labels': set({
|
|
||||||
}),
|
|
||||||
'name': None,
|
|
||||||
'options': dict({
|
|
||||||
}),
|
|
||||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
|
||||||
'original_icon': None,
|
|
||||||
'original_name': 'Wireless mode',
|
|
||||||
'platform': 'airos',
|
|
||||||
'previous_unique_id': None,
|
|
||||||
'suggested_object_id': None,
|
|
||||||
'supported_features': 0,
|
|
||||||
'translation_key': 'wireless_mode',
|
|
||||||
'unique_id': '01:23:45:67:89:AB_wireless_mode',
|
|
||||||
'unit_of_measurement': None,
|
|
||||||
})
|
|
||||||
# ---
|
|
||||||
# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-state]
|
|
||||||
StateSnapshot({
|
|
||||||
'attributes': ReadOnlyDict({
|
|
||||||
'device_class': 'enum',
|
|
||||||
'friendly_name': 'NanoStation 5AC ap name Wireless mode',
|
|
||||||
'options': list([
|
|
||||||
'ap_ptp',
|
|
||||||
'sta_ptp',
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
'context': <ANY>,
|
|
||||||
'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode',
|
|
||||||
'last_changed': <ANY>,
|
|
||||||
'last_reported': <ANY>,
|
|
||||||
'last_updated': <ANY>,
|
|
||||||
'state': 'ap_ptp',
|
|
||||||
})
|
|
||||||
# ---
|
|
||||||
# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry]
|
# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
@@ -4,9 +4,9 @@ from typing import Any
|
|||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
ConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
KeyDataMissingError,
|
AirOSKeyDataMissingError,
|
||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ async def test_form_duplicate_entry(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("exception", "error"),
|
("exception", "error"),
|
||||||
[
|
[
|
||||||
(ConnectionAuthenticationError, "invalid_auth"),
|
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
||||||
(DeviceConnectionError, "cannot_connect"),
|
(AirOSDeviceConnectionError, "cannot_connect"),
|
||||||
(KeyDataMissingError, "key_data_missing"),
|
(AirOSKeyDataMissingError, "key_data_missing"),
|
||||||
(Exception, "unknown"),
|
(Exception, "unknown"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -4,9 +4,9 @@ from datetime import timedelta
|
|||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
ConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
DataMissingError,
|
AirOSDataMissingError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
)
|
)
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
@@ -39,10 +39,10 @@ async def test_all_entities(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("exception"),
|
("exception"),
|
||||||
[
|
[
|
||||||
ConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
TimeoutError,
|
TimeoutError,
|
||||||
DeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
DataMissingError,
|
AirOSDataMissingError,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_sensor_update_exception_handling(
|
async def test_sensor_update_exception_handling(
|
||||||
|
94
tests/components/downloader/conftest.py
Normal file
94
tests/components/downloader/conftest.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"""Provide common fixtures for downloader tests."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests_mock import Mocker
|
||||||
|
|
||||||
|
from homeassistant.components.downloader.const import (
|
||||||
|
CONF_DOWNLOAD_DIR,
|
||||||
|
DOMAIN,
|
||||||
|
DOWNLOAD_COMPLETED_EVENT,
|
||||||
|
DOWNLOAD_FAILED_EVENT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the downloader integration for testing."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return mock_config_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
download_dir: Path,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Return a mocked config entry."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_DOWNLOAD_DIR: str(download_dir)},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def download_dir(tmp_path: Path) -> Path:
|
||||||
|
"""Return a download directory."""
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_download_request(
|
||||||
|
requests_mock: Mocker,
|
||||||
|
download_url: str,
|
||||||
|
) -> None:
|
||||||
|
"""Mock the download request."""
|
||||||
|
requests_mock.get(download_url, text="{'one': 1}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def download_url() -> str:
|
||||||
|
"""Return a mock download URL."""
|
||||||
|
return "http://example.com/file.txt"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def download_completed(hass: HomeAssistant) -> asyncio.Event:
|
||||||
|
"""Return an asyncio event to wait for download completion."""
|
||||||
|
download_event = asyncio.Event()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def download_set(event: Event[dict[str, str]]) -> None:
|
||||||
|
"""Set the event when download is completed."""
|
||||||
|
download_event.set()
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", download_set)
|
||||||
|
|
||||||
|
return download_event
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def download_failed(hass: HomeAssistant) -> asyncio.Event:
|
||||||
|
"""Return an asyncio event to wait for download failure."""
|
||||||
|
download_event = asyncio.Event()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def download_set(event: Event[dict[str, str]]) -> None:
|
||||||
|
"""Set the event when download has failed."""
|
||||||
|
download_event.set()
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", download_set)
|
||||||
|
|
||||||
|
return download_event
|
@@ -1,6 +1,8 @@
|
|||||||
"""Tests for the downloader component init."""
|
"""Tests for the downloader component init."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.downloader.const import (
|
from homeassistant.components.downloader.const import (
|
||||||
CONF_DOWNLOAD_DIR,
|
CONF_DOWNLOAD_DIR,
|
||||||
@@ -13,17 +15,57 @@ from homeassistant.core import HomeAssistant
|
|||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_initialization(hass: HomeAssistant) -> None:
|
@pytest.fixture
|
||||||
"""Test the initialization of the downloader component."""
|
def download_dir(tmp_path: Path, request: pytest.FixtureRequest) -> Path:
|
||||||
config_entry = MockConfigEntry(
|
"""Return a download directory."""
|
||||||
domain=DOMAIN,
|
if hasattr(request, "param"):
|
||||||
data={
|
return tmp_path / request.param
|
||||||
CONF_DOWNLOAD_DIR: "/test_dir",
|
return tmp_path
|
||||||
},
|
|
||||||
)
|
|
||||||
config_entry.add_to_hass(hass)
|
async def test_config_entry_setup(
|
||||||
with patch("os.path.isdir", return_value=True):
|
hass: HomeAssistant, setup_integration: MockConfigEntry
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
) -> None:
|
||||||
|
"""Test config entry setup."""
|
||||||
|
config_entry = setup_integration
|
||||||
|
|
||||||
assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE)
|
assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE)
|
||||||
assert config_entry.state is ConfigEntryState.LOADED
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entry_setup_relative_directory(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test config entry setup with a relative download directory."""
|
||||||
|
relative_directory = "downloads"
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
mock_config_entry,
|
||||||
|
data={**mock_config_entry.data, CONF_DOWNLOAD_DIR: relative_directory},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
# The config entry will fail to set up since the directory does not exist.
|
||||||
|
# This is not relevant for this test.
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
|
assert mock_config_entry.data[CONF_DOWNLOAD_DIR] == hass.config.path(
|
||||||
|
relative_directory
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"download_dir",
|
||||||
|
[
|
||||||
|
"not_existing_path",
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_config_entry_setup_not_existing_directory(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test config entry setup without existing download directory."""
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
assert not hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE)
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
|
54
tests/components/downloader/test_services.py
Normal file
54
tests/components/downloader/test_services.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Test downloader services."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from contextlib import AbstractContextManager, nullcontext as does_not_raise
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.downloader.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_integration")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("subdir", "expected_result"),
|
||||||
|
[
|
||||||
|
("test", does_not_raise()),
|
||||||
|
("test/path", does_not_raise()),
|
||||||
|
("~test/path", pytest.raises(ServiceValidationError)),
|
||||||
|
("~/../test/path", pytest.raises(ServiceValidationError)),
|
||||||
|
("../test/path", pytest.raises(ServiceValidationError)),
|
||||||
|
(".../test/path", pytest.raises(ServiceValidationError)),
|
||||||
|
("/test/path", pytest.raises(ServiceValidationError)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_download_invalid_subdir(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
download_completed: asyncio.Event,
|
||||||
|
download_failed: asyncio.Event,
|
||||||
|
download_url: str,
|
||||||
|
subdir: str,
|
||||||
|
expected_result: AbstractContextManager,
|
||||||
|
) -> None:
|
||||||
|
"""Test service invalid subdirectory."""
|
||||||
|
|
||||||
|
async def call_service() -> None:
|
||||||
|
"""Call the download service."""
|
||||||
|
completed = hass.async_create_task(download_completed.wait())
|
||||||
|
failed = hass.async_create_task(download_failed.wait())
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
"download_file",
|
||||||
|
{
|
||||||
|
"url": download_url,
|
||||||
|
"subdir": subdir,
|
||||||
|
"filename": "file.txt",
|
||||||
|
"overwrite": True,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await asyncio.wait((completed, failed), return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
|
||||||
|
with expected_result:
|
||||||
|
await call_service()
|
@@ -47,6 +47,54 @@
|
|||||||
'state': 'unavailable',
|
'state': 'unavailable',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_button_snapshot[button.test_mower_1_reset_cutting_blade_usage_time-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'button',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'button.test_mower_1_reset_cutting_blade_usage_time',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Reset cutting blade usage time',
|
||||||
|
'platform': 'husqvarna_automower',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'reset_cutting_blade_usage_time',
|
||||||
|
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_reset_cutting_blade_usage_time',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_button_snapshot[button.test_mower_1_reset_cutting_blade_usage_time-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Test Mower 1 Reset cutting blade usage time',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'button.test_mower_1_reset_cutting_blade_usage_time',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_button_snapshot[button.test_mower_1_sync_clock-entry]
|
# name: test_button_snapshot[button.test_mower_1_sync_clock-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
@@ -28,7 +28,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.freeze_time(datetime.datetime(2023, 6, 5, tzinfo=datetime.UTC))
|
@pytest.mark.freeze_time(datetime.datetime(2023, 6, 5, tzinfo=datetime.UTC))
|
||||||
async def test_button_states_and_commands(
|
async def test_button_error_confirm(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
@@ -58,42 +58,43 @@ async def test_button_states_and_commands(
|
|||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
domain="button",
|
|
||||||
service=SERVICE_PRESS,
|
|
||||||
target={ATTR_ENTITY_ID: entity_id},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
mock_automower_client.commands.error_confirm.assert_called_once_with(TEST_MOWER_ID)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
assert state.state == "2023-06-05T00:16:00+00:00"
|
|
||||||
mock_automower_client.commands.error_confirm.side_effect = ApiError("Test error")
|
|
||||||
with pytest.raises(
|
|
||||||
HomeAssistantError,
|
|
||||||
match="Failed to send command: Test error",
|
|
||||||
):
|
|
||||||
await hass.services.async_call(
|
|
||||||
domain="button",
|
|
||||||
service=SERVICE_PRESS,
|
|
||||||
target={ATTR_ENTITY_ID: entity_id},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("entity_id", "name", "expected_command"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"button.test_mower_1_confirm_error",
|
||||||
|
"Test Mower 1 Confirm error",
|
||||||
|
"error_confirm",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"button.test_mower_1_sync_clock",
|
||||||
|
"Test Mower 1 Sync clock",
|
||||||
|
"set_datetime",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"button.test_mower_1_reset_cutting_blade_usage_time",
|
||||||
|
"Test Mower 1 Reset cutting blade usage time",
|
||||||
|
"reset_cutting_blade_usage_time",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC))
|
@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC))
|
||||||
async def test_sync_clock(
|
async def test_button_commands(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
freezer: FrozenDateTimeFactory,
|
|
||||||
values: dict[str, MowerAttributes],
|
values: dict[str, MowerAttributes],
|
||||||
|
entity_id: str,
|
||||||
|
name: str,
|
||||||
|
expected_command: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test sync clock button command."""
|
"""Test Automower button commands."""
|
||||||
entity_id = "button.test_mower_1_sync_clock"
|
values[TEST_MOWER_ID].mower.is_error_confirmable = True
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state.name == "Test Mower 1 Sync clock"
|
assert state.name == name
|
||||||
|
|
||||||
mock_automower_client.get_status.return_value = values
|
mock_automower_client.get_status.return_value = values
|
||||||
|
|
||||||
@@ -103,11 +104,15 @@ async def test_sync_clock(
|
|||||||
{ATTR_ENTITY_ID: entity_id},
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
mock_automower_client.commands.set_datetime.assert_called_once_with(TEST_MOWER_ID)
|
|
||||||
|
command_mock = getattr(mock_automower_client.commands, expected_command)
|
||||||
|
command_mock.assert_called_once_with(TEST_MOWER_ID)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state.state == "2024-02-29T11:00:00+00:00"
|
assert state.state == "2024-02-29T11:00:00+00:00"
|
||||||
mock_automower_client.commands.set_datetime.side_effect = ApiError("Test error")
|
command_mock.reset_mock()
|
||||||
|
command_mock.side_effect = ApiError("Test error")
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
match="Failed to send command: Test error",
|
match="Failed to send command: Test error",
|
||||||
|
@@ -14,7 +14,7 @@ from aioautomower.exceptions import (
|
|||||||
HusqvarnaTimeoutError,
|
HusqvarnaTimeoutError,
|
||||||
HusqvarnaWSServerHandshakeError,
|
HusqvarnaWSServerHandshakeError,
|
||||||
)
|
)
|
||||||
from aioautomower.model import Calendar, MowerAttributes, WorkArea
|
from aioautomower.model import Calendar, MowerAttributes, MowerStates, WorkArea
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
@@ -484,3 +484,212 @@ async def test_add_and_remove_work_area(
|
|||||||
- ADDITIONAL_NUMBER_ENTITIES
|
- ADDITIONAL_NUMBER_ENTITIES
|
||||||
- ADDITIONAL_SENSOR_ENTITIES
|
- ADDITIONAL_SENSOR_ENTITIES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("mower1_connected", "mower1_state", "mower2_connected", "mower2_state"),
|
||||||
|
[
|
||||||
|
(True, MowerStates.OFF, False, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.PAUSED, False, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, True, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, False, MowerStates.PAUSED), # False
|
||||||
|
(True, MowerStates.OFF, True, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, False, MowerStates.OFF), # False
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_dynamic_polling(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_automower_client,
|
||||||
|
mock_config_entry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
values: dict[str, MowerAttributes],
|
||||||
|
mower1_connected: bool,
|
||||||
|
mower1_state: MowerStates,
|
||||||
|
mower2_connected: bool,
|
||||||
|
mower2_state: MowerStates,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the ws_ready_callback triggers an attempt to start the Watchdog task.
|
||||||
|
|
||||||
|
and that the pong callback stops polling when all mowers are inactive.
|
||||||
|
"""
|
||||||
|
websocket_values = deepcopy(values)
|
||||||
|
poll_values = deepcopy(values)
|
||||||
|
callback_holder: dict[str, Callable] = {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def fake_register_websocket_response(
|
||||||
|
cb: Callable[[dict[str, MowerAttributes]], None],
|
||||||
|
) -> None:
|
||||||
|
callback_holder["data_cb"] = cb
|
||||||
|
|
||||||
|
mock_automower_client.register_data_callback.side_effect = (
|
||||||
|
fake_register_websocket_response
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None:
|
||||||
|
callback_holder["ws_ready_cb"] = cb
|
||||||
|
|
||||||
|
mock_automower_client.register_ws_ready_callback.side_effect = (
|
||||||
|
fake_register_ws_ready_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert "ws_ready_cb" in callback_holder, "ws_ready_callback was not registered"
|
||||||
|
callback_holder["ws_ready_cb"]()
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 2
|
||||||
|
|
||||||
|
# websocket is still active, but mowers are inactive -> no polling required
|
||||||
|
poll_values[TEST_MOWER_ID].metadata.connected = mower1_connected
|
||||||
|
poll_values[TEST_MOWER_ID].mower.state = mower1_state
|
||||||
|
poll_values["1234"].metadata.connected = mower2_connected
|
||||||
|
poll_values["1234"].mower.state = mower2_state
|
||||||
|
|
||||||
|
mock_automower_client.get_status.return_value = poll_values
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 3
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 4
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 4
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 4
|
||||||
|
|
||||||
|
# websocket is still active, and mowers are active -> polling required
|
||||||
|
mock_automower_client.get_status.reset_mock()
|
||||||
|
assert mock_automower_client.get_status.call_count == 0
|
||||||
|
poll_values[TEST_MOWER_ID].metadata.connected = True
|
||||||
|
poll_values[TEST_MOWER_ID].mower.state = MowerStates.PAUSED
|
||||||
|
poll_values["1234"].metadata.connected = False
|
||||||
|
poll_values["1234"].mower.state = MowerStates.OFF
|
||||||
|
websocket_values = deepcopy(poll_values)
|
||||||
|
callback_holder["data_cb"](websocket_values)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("mower1_connected", "mower1_state", "mower2_connected", "mower2_state"),
|
||||||
|
[
|
||||||
|
(True, MowerStates.OFF, False, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.PAUSED, False, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, True, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, False, MowerStates.PAUSED), # False
|
||||||
|
(True, MowerStates.OFF, True, MowerStates.OFF), # False
|
||||||
|
(False, MowerStates.OFF, False, MowerStates.OFF), # False
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_websocket_watchdog(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_automower_client,
|
||||||
|
mock_config_entry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
values: dict[str, MowerAttributes],
|
||||||
|
mower1_connected: bool,
|
||||||
|
mower1_state: MowerStates,
|
||||||
|
mower2_connected: bool,
|
||||||
|
mower2_state: MowerStates,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the ws_ready_callback triggers an attempt to start the Watchdog task.
|
||||||
|
|
||||||
|
and that the pong callback stops polling when all mowers are inactive.
|
||||||
|
"""
|
||||||
|
poll_values = deepcopy(values)
|
||||||
|
callback_holder: dict[str, Callable] = {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def fake_register_websocket_response(
|
||||||
|
cb: Callable[[dict[str, MowerAttributes]], None],
|
||||||
|
) -> None:
|
||||||
|
callback_holder["data_cb"] = cb
|
||||||
|
|
||||||
|
mock_automower_client.register_data_callback.side_effect = (
|
||||||
|
fake_register_websocket_response
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def fake_register_ws_ready_callback(cb: Callable[[], None]) -> None:
|
||||||
|
callback_holder["ws_ready_cb"] = cb
|
||||||
|
|
||||||
|
mock_automower_client.register_ws_ready_callback.side_effect = (
|
||||||
|
fake_register_ws_ready_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert "ws_ready_cb" in callback_holder, "ws_ready_callback was not registered"
|
||||||
|
callback_holder["ws_ready_cb"]()
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 2
|
||||||
|
|
||||||
|
# websocket is still active, but mowers are inactive -> no polling required
|
||||||
|
poll_values[TEST_MOWER_ID].metadata.connected = mower1_connected
|
||||||
|
poll_values[TEST_MOWER_ID].mower.state = mower1_state
|
||||||
|
poll_values["1234"].metadata.connected = mower2_connected
|
||||||
|
poll_values["1234"].mower.state = mower2_state
|
||||||
|
|
||||||
|
mock_automower_client.get_status.return_value = poll_values
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 3
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 4
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 4
|
||||||
|
|
||||||
|
# Simulate Pong loss and reset mock -> polling required
|
||||||
|
mock_automower_client.send_empty_message.return_value = False
|
||||||
|
mock_automower_client.get_status.reset_mock()
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 0
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_automower_client.get_status.call_count == 2
|
||||||
|
@@ -203,7 +203,7 @@
|
|||||||
"1/6/65528": [],
|
"1/6/65528": [],
|
||||||
"1/6/65529": [0, 1, 2],
|
"1/6/65529": [0, 1, 2],
|
||||||
"1/6/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
"1/6/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
||||||
"1/8/0": 254,
|
"1/8/0": 200,
|
||||||
"1/8/15": 0,
|
"1/8/15": 0,
|
||||||
"1/8/17": 0,
|
"1/8/17": 0,
|
||||||
"1/8/65532": 0,
|
"1/8/65532": 0,
|
||||||
|
@@ -2189,7 +2189,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '127.0',
|
'state': '100.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry]
|
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry]
|
||||||
|
@@ -172,7 +172,7 @@ async def test_pump_level(
|
|||||||
# CurrentLevel on LevelControl cluster
|
# CurrentLevel on LevelControl cluster
|
||||||
state = hass.states.get("number.mock_pump_setpoint")
|
state = hass.states.get("number.mock_pump_setpoint")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "127.0"
|
assert state.state == "100.0"
|
||||||
|
|
||||||
set_node_attribute(matter_node, 1, 8, 0, 100)
|
set_node_attribute(matter_node, 1, 8, 0, 100)
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import AsyncGenerator, Generator
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
@@ -130,6 +130,28 @@ def mock_smile_config_flow() -> Generator[MagicMock]:
|
|||||||
yield api
|
yield api
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def platforms() -> list[str]:
|
||||||
|
"""Fixture for platforms."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
platforms,
|
||||||
|
) -> AsyncGenerator[None]:
|
||||||
|
"""Set up one or all platforms."""
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms):
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
yield mock_config_entry
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_smile_adam() -> Generator[MagicMock]:
|
def mock_smile_adam() -> Generator[MagicMock]:
|
||||||
"""Create a Mock Adam environment for testing exceptions."""
|
"""Create a Mock Adam environment for testing exceptions."""
|
||||||
|
50
tests/components/plugwise/snapshots/test_button.ambr
Normal file
50
tests/components/plugwise/snapshots/test_button.ambr
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_adam_button_snapshot[platforms0][button.adam_reboot-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'button',
|
||||||
|
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||||
|
'entity_id': 'button.adam_reboot',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Reboot',
|
||||||
|
'platform': 'plugwise',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'reboot',
|
||||||
|
'unique_id': 'fe799307f1624099878210aa0b9f1475-reboot',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_adam_button_snapshot[platforms0][button.adam_reboot-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'restart',
|
||||||
|
'friendly_name': 'Adam Reboot',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'button.adam_reboot',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
@@ -2,32 +2,34 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from homeassistant.components.button import (
|
import pytest
|
||||||
DOMAIN as BUTTON_DOMAIN,
|
from syrupy.assertion import SnapshotAssertion
|
||||||
SERVICE_PRESS,
|
|
||||||
ButtonDeviceClass,
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||||
)
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
async def test_adam_reboot_button(
|
@pytest.mark.parametrize("platforms", [(BUTTON_DOMAIN,)])
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_adam_button_snapshot(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_smile_adam: MagicMock,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
setup_platform: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test Adam button snapshot."""
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adam_press_reboot_button(
|
||||||
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test creation of button entities."""
|
"""Test pressing of button entity."""
|
||||||
state = hass.states.get("button.adam_reboot")
|
|
||||||
assert state
|
|
||||||
assert state.state == STATE_UNKNOWN
|
|
||||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART
|
|
||||||
|
|
||||||
registry = er.async_get(hass)
|
|
||||||
entry = registry.async_get("button.adam_reboot")
|
|
||||||
assert entry
|
|
||||||
assert entry.unique_id == "fe799307f1624099878210aa0b9f1475-reboot"
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
BUTTON_DOMAIN,
|
BUTTON_DOMAIN,
|
||||||
SERVICE_PRESS,
|
SERVICE_PRESS,
|
||||||
|
@@ -90,8 +90,8 @@
|
|||||||
'null': 5,
|
'null': 5,
|
||||||
}),
|
}),
|
||||||
'GetAiCfg': dict({
|
'GetAiCfg': dict({
|
||||||
'0': 4,
|
'0': 2,
|
||||||
'null': 4,
|
'null': 2,
|
||||||
}),
|
}),
|
||||||
'GetAudioAlarm': dict({
|
'GetAudioAlarm': dict({
|
||||||
'0': 1,
|
'0': 1,
|
||||||
@@ -177,10 +177,6 @@
|
|||||||
'0': 2,
|
'0': 2,
|
||||||
'null': 2,
|
'null': 2,
|
||||||
}),
|
}),
|
||||||
'GetPtzTraceSection': dict({
|
|
||||||
'0': 2,
|
|
||||||
'null': 2,
|
|
||||||
}),
|
|
||||||
'GetPush': dict({
|
'GetPush': dict({
|
||||||
'0': 1,
|
'0': 1,
|
||||||
'null': 2,
|
'null': 2,
|
||||||
@@ -196,8 +192,8 @@
|
|||||||
'null': 1,
|
'null': 1,
|
||||||
}),
|
}),
|
||||||
'GetWhiteLed': dict({
|
'GetWhiteLed': dict({
|
||||||
'0': 3,
|
'0': 2,
|
||||||
'null': 3,
|
'null': 2,
|
||||||
}),
|
}),
|
||||||
'GetZoomFocus': dict({
|
'GetZoomFocus': dict({
|
||||||
'0': 2,
|
'0': 2,
|
||||||
|
@@ -15,7 +15,7 @@ from homeassistant.components.vacuum import (
|
|||||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||||
from homeassistant.helpers.entity_component import async_update_entity
|
from homeassistant.helpers.entity_component import async_update_entity
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
@@ -589,6 +589,40 @@ async def test_battery_level_template(
|
|||||||
_verify(hass, STATE_UNKNOWN, expected)
|
_verify(hass, STATE_UNKNOWN, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("count", "state_template", "extra_config", "attribute_template"),
|
||||||
|
[(1, "{{ states('sensor.test_state') }}", {}, "{{ 50 }}")],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("style", "attribute"),
|
||||||
|
[
|
||||||
|
(ConfigurationStyle.LEGACY, "battery_level_template"),
|
||||||
|
(ConfigurationStyle.MODERN, "battery_level"),
|
||||||
|
(ConfigurationStyle.TRIGGER, "battery_level"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
|
||||||
|
async def test_battery_level_template_repair(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
issue_registry: ir.IssueRegistry,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test battery_level template raises issue."""
|
||||||
|
# Ensure trigger entity templates are rendered
|
||||||
|
hass.states.async_set(TEST_STATE_SENSOR, VacuumActivity.DOCKED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
issue = issue_registry.async_get_issue(
|
||||||
|
"template", f"deprecated_battery_level_{TEST_ENTITY_ID}"
|
||||||
|
)
|
||||||
|
assert issue.domain == "template"
|
||||||
|
assert issue.severity == ir.IssueSeverity.WARNING
|
||||||
|
assert issue.translation_placeholders["entity_name"] == TEST_OBJECT_ID
|
||||||
|
assert issue.translation_placeholders["entity_id"] == TEST_ENTITY_ID
|
||||||
|
assert "Detected that integration 'template' is setting the" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("count", "state_template", "extra_config"),
|
("count", "state_template", "extra_config"),
|
||||||
[
|
[
|
||||||
|
@@ -41,6 +41,11 @@ DEVICE_MOCKS = {
|
|||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
],
|
],
|
||||||
|
"cs_qhxmvae667uap4zh": [
|
||||||
|
# https://github.com/home-assistant/core/issues/141278
|
||||||
|
Platform.FAN,
|
||||||
|
Platform.HUMIDIFIER,
|
||||||
|
],
|
||||||
"cs_vmxuxszzjwp5smli": [
|
"cs_vmxuxszzjwp5smli": [
|
||||||
# https://github.com/home-assistant/core/issues/119865
|
# https://github.com/home-assistant/core/issues/119865
|
||||||
Platform.FAN,
|
Platform.FAN,
|
||||||
@@ -214,6 +219,16 @@ DEVICE_MOCKS = {
|
|||||||
# https://github.com/home-assistant/core/issues/143499
|
# https://github.com/home-assistant/core/issues/143499
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
],
|
],
|
||||||
|
"fs_g0ewlb1vmwqljzji": [
|
||||||
|
# https://github.com/home-assistant/core/issues/141231
|
||||||
|
Platform.FAN,
|
||||||
|
Platform.LIGHT,
|
||||||
|
Platform.SELECT,
|
||||||
|
],
|
||||||
|
"fs_ibytpo6fpnugft1c": [
|
||||||
|
# https://github.com/home-assistant/core/issues/135541
|
||||||
|
Platform.FAN,
|
||||||
|
],
|
||||||
"gyd_lgekqfxdabipm3tn": [
|
"gyd_lgekqfxdabipm3tn": [
|
||||||
# https://github.com/home-assistant/core/issues/133173
|
# https://github.com/home-assistant/core/issues/133173
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
@@ -227,6 +242,11 @@ DEVICE_MOCKS = {
|
|||||||
# https://github.com/home-assistant/core/issues/148347
|
# https://github.com/home-assistant/core/issues/148347
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
],
|
],
|
||||||
|
"kj_CAjWAxBUZt7QZHfz": [
|
||||||
|
# https://github.com/home-assistant/core/issues/146023
|
||||||
|
Platform.FAN,
|
||||||
|
Platform.SWITCH,
|
||||||
|
],
|
||||||
"kj_yrzylxax1qspdgpp": [
|
"kj_yrzylxax1qspdgpp": [
|
||||||
# https://github.com/orgs/home-assistant/discussions/61
|
# https://github.com/orgs/home-assistant/discussions/61
|
||||||
Platform.FAN,
|
Platform.FAN,
|
||||||
@@ -294,6 +314,15 @@ DEVICE_MOCKS = {
|
|||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
],
|
],
|
||||||
|
"sd_lr33znaodtyarrrz": [
|
||||||
|
# https://github.com/home-assistant/core/issues/141278
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
Platform.VACUUM,
|
||||||
|
],
|
||||||
"sfkzq_o6dagifntoafakst": [
|
"sfkzq_o6dagifntoafakst": [
|
||||||
# https://github.com/home-assistant/core/issues/148116
|
# https://github.com/home-assistant/core/issues/148116
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
@@ -306,16 +335,27 @@ DEVICE_MOCKS = {
|
|||||||
],
|
],
|
||||||
"sp_drezasavompxpcgm": [
|
"sp_drezasavompxpcgm": [
|
||||||
# https://github.com/home-assistant/core/issues/149704
|
# https://github.com/home-assistant/core/issues/149704
|
||||||
|
Platform.CAMERA,
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
],
|
],
|
||||||
"sp_rjKXWRohlvOTyLBu": [
|
"sp_rjKXWRohlvOTyLBu": [
|
||||||
# https://github.com/home-assistant/core/issues/149704
|
# https://github.com/home-assistant/core/issues/149704
|
||||||
|
Platform.CAMERA,
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
],
|
],
|
||||||
|
"sp_sdd5f5f2dl5wydjf": [
|
||||||
|
# https://github.com/home-assistant/core/issues/144087
|
||||||
|
Platform.CAMERA,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SIREN,
|
||||||
|
Platform.SWITCH,
|
||||||
|
],
|
||||||
"tdq_cq1p0nt0a4rixnex": [
|
"tdq_cq1p0nt0a4rixnex": [
|
||||||
# https://github.com/home-assistant/core/issues/146845
|
# https://github.com/home-assistant/core/issues/146845
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
@@ -335,6 +375,10 @@ DEVICE_MOCKS = {
|
|||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
],
|
],
|
||||||
|
"wg2_nwxr8qcu4seltoro": [
|
||||||
|
# https://github.com/orgs/home-assistant/discussions/430
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
],
|
||||||
"wk_fi6dne5tu4t1nm6j": [
|
"wk_fi6dne5tu4t1nm6j": [
|
||||||
# https://github.com/orgs/home-assistant/discussions/243
|
# https://github.com/orgs/home-assistant/discussions/243
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1729466466688hgsTp2",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaus.com",
|
"endpoint": "https://apigw.tuyaus.com",
|
||||||
"terminal_id": "1732306182276g6jQLp",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "mock_terminal_id",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
32
tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json
Normal file
32
tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
|
"terminal_id": "REDACTED",
|
||||||
|
"mqtt_connected": true,
|
||||||
|
"disabled_by": null,
|
||||||
|
"disabled_polling": false,
|
||||||
|
"id": "28403630e8db84b7a963",
|
||||||
|
"name": "DryFix",
|
||||||
|
"category": "cs",
|
||||||
|
"product_id": "qhxmvae667uap4zh",
|
||||||
|
"product_name": "",
|
||||||
|
"online": false,
|
||||||
|
"sub": false,
|
||||||
|
"time_zone": "+01:00",
|
||||||
|
"active_time": "2024-04-03T13:10:02+00:00",
|
||||||
|
"create_time": "2024-04-03T13:10:02+00:00",
|
||||||
|
"update_time": "2024-04-03T13:10:02+00:00",
|
||||||
|
"function": {},
|
||||||
|
"status_range": {
|
||||||
|
"fault": {
|
||||||
|
"type": "Bitmap",
|
||||||
|
"value": {
|
||||||
|
"label": ["E1", "E2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"fault": 0
|
||||||
|
},
|
||||||
|
"set_up": true,
|
||||||
|
"support_local": true
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "mock_terminal_id",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1750837476328i3TNXQ",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1747045731408d0tb5M",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1751729689584Vh0VoL",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaus.com",
|
"endpoint": "https://apigw.tuyaus.com",
|
||||||
"terminal_id": "1742695000703Ozq34h",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1733006572651YokbqV",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": null,
|
"mqtt_connected": null,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
134
tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json
Normal file
134
tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
|
"terminal_id": "REDACTED",
|
||||||
|
"mqtt_connected": true,
|
||||||
|
"disabled_by": null,
|
||||||
|
"disabled_polling": false,
|
||||||
|
"id": "XXX",
|
||||||
|
"name": "Ceiling Fan With Light",
|
||||||
|
"category": "fs",
|
||||||
|
"product_id": "g0ewlb1vmwqljzji",
|
||||||
|
"product_name": "Ceiling Fan With Light",
|
||||||
|
"online": true,
|
||||||
|
"sub": false,
|
||||||
|
"time_zone": "+01:00",
|
||||||
|
"active_time": "2025-03-22T22:57:04+00:00",
|
||||||
|
"create_time": "2025-03-22T22:57:04+00:00",
|
||||||
|
"update_time": "2025-03-22T22:57:04+00:00",
|
||||||
|
"function": {
|
||||||
|
"switch": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["normal", "sleep", "nature"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fan_speed": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["1", "2", "3", "4", "5", "6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fan_direction": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["forward", "reverse"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"bright_value": {
|
||||||
|
"type": "Integer",
|
||||||
|
"value": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"scale": 0,
|
||||||
|
"step": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temp_value": {
|
||||||
|
"type": "Integer",
|
||||||
|
"value": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"scale": 0,
|
||||||
|
"step": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"countdown_set": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["cancel", "1h", "2h", "4h", "8h"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status_range": {
|
||||||
|
"switch": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["normal", "sleep", "nature"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fan_speed": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["1", "2", "3", "4", "5", "6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fan_direction": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["forward", "reverse"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"bright_value": {
|
||||||
|
"type": "Integer",
|
||||||
|
"value": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"scale": 0,
|
||||||
|
"step": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temp_value": {
|
||||||
|
"type": "Integer",
|
||||||
|
"value": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"scale": 0,
|
||||||
|
"step": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"countdown_set": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["cancel", "1h", "2h", "4h", "8h"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"switch": true,
|
||||||
|
"mode": "normal",
|
||||||
|
"fan_speed": 1,
|
||||||
|
"fan_direction": "reverse",
|
||||||
|
"light": true,
|
||||||
|
"bright_value": 100,
|
||||||
|
"temp_value": 0,
|
||||||
|
"countdown_set": "off"
|
||||||
|
},
|
||||||
|
"set_up": true,
|
||||||
|
"support_local": true
|
||||||
|
}
|
23
tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json
Normal file
23
tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
|
"terminal_id": "REDACTED",
|
||||||
|
"mqtt_connected": true,
|
||||||
|
"disabled_by": null,
|
||||||
|
"disabled_polling": false,
|
||||||
|
"id": "10706550a4e57c88b93a",
|
||||||
|
"name": "Ventilador Cama",
|
||||||
|
"category": "fs",
|
||||||
|
"product_id": "ibytpo6fpnugft1c",
|
||||||
|
"product_name": "Tower bladeless fan ",
|
||||||
|
"online": true,
|
||||||
|
"sub": false,
|
||||||
|
"time_zone": "+01:00",
|
||||||
|
"active_time": "2025-01-10T18:47:46+00:00",
|
||||||
|
"create_time": "2025-01-10T18:47:46+00:00",
|
||||||
|
"update_time": "2025-01-10T18:47:46+00:00",
|
||||||
|
"function": {},
|
||||||
|
"status_range": {},
|
||||||
|
"status": {},
|
||||||
|
"set_up": true,
|
||||||
|
"support_local": true
|
||||||
|
}
|
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaus.com",
|
"endpoint": "https://apigw.tuyaus.com",
|
||||||
"terminal_id": "1732306182276g6jQLp",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
|
||||||
"id": "eb3e988f33c233290cfs3l",
|
"id": "eb3e988f33c233290cfs3l",
|
||||||
"name": "Colorful PIR Night Light",
|
"name": "Colorful PIR Night Light",
|
||||||
"category": "gyd",
|
"category": "gyd",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaus.com",
|
"endpoint": "https://apigw.tuyaus.com",
|
||||||
"terminal_id": "1750526976566fMhqJs",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": true,
|
"disabled_polling": true,
|
||||||
|
86
tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json
Normal file
86
tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
|
"terminal_id": "REDACTED",
|
||||||
|
"mqtt_connected": true,
|
||||||
|
"disabled_by": null,
|
||||||
|
"disabled_polling": false,
|
||||||
|
"id": "152027113c6105cce49c",
|
||||||
|
"name": "HL400",
|
||||||
|
"category": "kj",
|
||||||
|
"product_id": "CAjWAxBUZt7QZHfz",
|
||||||
|
"product_name": "air purifier",
|
||||||
|
"online": true,
|
||||||
|
"sub": false,
|
||||||
|
"time_zone": "+01:00",
|
||||||
|
"active_time": "2025-05-13T11:02:55+00:00",
|
||||||
|
"create_time": "2025-05-13T11:02:55+00:00",
|
||||||
|
"update_time": "2025-05-13T11:02:55+00:00",
|
||||||
|
"function": {
|
||||||
|
"switch": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["1", "2", "3"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anion": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"lock": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"uv": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status_range": {
|
||||||
|
"uv": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"lock": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"anion": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": {
|
||||||
|
"range": ["1", "2", "3"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"pm25": {
|
||||||
|
"type": "Integer",
|
||||||
|
"value": {
|
||||||
|
"unit": "",
|
||||||
|
"min": 0,
|
||||||
|
"max": 500,
|
||||||
|
"scale": 0,
|
||||||
|
"step": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"switch": true,
|
||||||
|
"lock": false,
|
||||||
|
"anion": true,
|
||||||
|
"speed": 3,
|
||||||
|
"uv": true,
|
||||||
|
"pm25": 45
|
||||||
|
},
|
||||||
|
"set_up": true,
|
||||||
|
"support_local": true
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "CENSORED",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "mock_terminal_id",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "mock_terminal_id",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1737479380414pasuj4",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1751921699759JsVujI",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaeu.com",
|
"endpoint": "https://apigw.tuyaeu.com",
|
||||||
"terminal_id": "1708196692712PHOeqy",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "https://apigw.tuyaus.com",
|
"endpoint": "https://apigw.tuyaus.com",
|
||||||
"terminal_id": "17421891051898r7yM6",
|
"terminal_id": "REDACTED",
|
||||||
"mqtt_connected": true,
|
"mqtt_connected": true,
|
||||||
"disabled_by": null,
|
"disabled_by": null,
|
||||||
"disabled_polling": false,
|
"disabled_polling": false,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user