forked from home-assistant/core
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5f86bff45 | |||
| d991970754 | |||
| d745b44180 | |||
| 602fcd6b1b | |||
| b39b0a960e | |||
| 40bb796f03 | |||
| 2801ba6cad | |||
| 5da0ef36ea | |||
| d861292900 | |||
| a3fda43c64 | |||
| 8705a26a1a | |||
| 2b1c45c28c | |||
| 0cf3825183 | |||
| 413e1c97d7 | |||
| 3b27a3aabf | |||
| 4509e13ceb | |||
| 33bf8c600b | |||
| b508875f17 | |||
| ac963a2b6e | |||
| 13029cf26f | |||
| f39a6b96ff | |||
| c6a17d6832 | |||
| 74c0552a12 | |||
| f24b514c9a | |||
| e1c47fdb61 | |||
| 93baf24394 | |||
| a4e236d0b9 | |||
| 421fa5b035 | |||
| 3d3fecbd23 | |||
| 468be632fd | |||
| 74ccdcda68 | |||
| 5cc61acfb2 | |||
| 02d55a8e49 | |||
| f4e3ef6b51 | |||
| 7740539df0 | |||
| b077bf9b86 | |||
| 23f2898836 | |||
| e6638ca356 | |||
| 93d52d8835 | |||
| 26e08abb9a | |||
| 6a573b507e | |||
| 2b39550e55 | |||
| 0e50baf007 | |||
| 286de1f051 | |||
| 3e23996247 | |||
| 7a658117bb | |||
| 49388eab3a | |||
| e6fcc6b73c | |||
| e00012289d | |||
| f373f1abd5 | |||
| 2c43672a8a | |||
| 7a6327d7e2 | |||
| ee8f63b9c9 | |||
| 28e0f5e104 | |||
| eb036af410 | |||
| 4bb6fec1d6 | |||
| dbd5511e5e | |||
| 580065e946 | |||
| 4a31cb0ad8 | |||
| 5a63079c80 | |||
| 902bd521d2 | |||
| aff4d537a7 | |||
| 4f00cc9faa | |||
| 2a99fea1de | |||
| 9aeba6221b | |||
| bb2a89f065 | |||
| f92298c6fc | |||
| 6ff55a6505 |
@@ -420,7 +420,6 @@ omit =
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
homeassistant/components/glances/sensor.py
|
||||
homeassistant/components/goalfeed/*
|
||||
homeassistant/components/goodwe/__init__.py
|
||||
homeassistant/components/goodwe/button.py
|
||||
homeassistant/components/goodwe/coordinator.py
|
||||
|
||||
@@ -391,6 +391,7 @@ def async_enable_logging(
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
sys.excepthook = lambda *args: logging.getLogger(None).exception(
|
||||
"Uncaught exception", exc_info=args # type: ignore[arg-type]
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -71,7 +72,7 @@ class AbodeSensor(AbodeDevice, SensorEntity):
|
||||
elif description.key == CONST.HUMI_STATUS_KEY:
|
||||
self._attr_native_unit_of_measurement = device.humidity_unit
|
||||
elif description.key == CONST.LUX_STATUS_KEY:
|
||||
self._attr_native_unit_of_measurement = device.lux_unit
|
||||
self._attr_native_unit_of_measurement = LIGHT_LUX
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.1"]
|
||||
"requirements": ["aioairzone==0.6.3"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.1.7"]
|
||||
"requirements": ["aioairzone-cloud==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ async def async_setup_entry(
|
||||
|
||||
tasks = []
|
||||
for heater in data_connection.get_devices():
|
||||
tasks.append(heater.update_device_info())
|
||||
tasks.append(asyncio.create_task(heater.update_device_info()))
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
devs = []
|
||||
|
||||
@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.host = discovery_info.host
|
||||
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
|
||||
self.mac = discovery_info.properties.get("bt")
|
||||
assert self.mac
|
||||
if not self.mac:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
await self.async_set_unique_id(format_mac(self.mac))
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.host, CONF_NAME: self.name}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aiobafi6==0.8.0"],
|
||||
"requirements": ["aiobafi6==0.8.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_api._tcp.local.",
|
||||
|
||||
@@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
|
||||
_LOGGER.info(
|
||||
"Start migration of Alexa settings from v%s to v%s",
|
||||
self._prefs.alexa_settings_version,
|
||||
ALEXA_SETTINGS_VERSION,
|
||||
)
|
||||
if self._prefs.alexa_settings_version < 2 or (
|
||||
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
||||
self._prefs.alexa_settings_version < 3
|
||||
@@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
):
|
||||
self._migrate_alexa_entity_settings_v1()
|
||||
|
||||
_LOGGER.info(
|
||||
"Finished migration of Alexa settings from v%s to v%s",
|
||||
self._prefs.alexa_settings_version,
|
||||
ALEXA_SETTINGS_VERSION,
|
||||
)
|
||||
await self._prefs.async_update(
|
||||
alexa_settings_version=ALEXA_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
@@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
if domain in SUPPORTED_DOMAINS:
|
||||
return True
|
||||
|
||||
device_class = get_device_class(hass, entity_id)
|
||||
try:
|
||||
device_class = get_device_class(hass, entity_id)
|
||||
except HomeAssistantError:
|
||||
# The entity no longer exists
|
||||
return False
|
||||
|
||||
if (
|
||||
domain == "binary_sensor"
|
||||
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
|
||||
@@ -208,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
|
||||
_LOGGER.info(
|
||||
"Start migration of Google Assistant settings from v%s to v%s",
|
||||
self._prefs.google_settings_version,
|
||||
GOOGLE_SETTINGS_VERSION,
|
||||
)
|
||||
if self._prefs.google_settings_version < 2 or (
|
||||
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
||||
self._prefs.google_settings_version < 3
|
||||
@@ -220,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
):
|
||||
self._migrate_google_entity_settings_v1()
|
||||
|
||||
_LOGGER.info(
|
||||
"Finished migration of Google Assistant settings from v%s to v%s",
|
||||
self._prefs.google_settings_version,
|
||||
GOOGLE_SETTINGS_VERSION,
|
||||
)
|
||||
await self._prefs.async_update(
|
||||
google_settings_version=GOOGLE_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
@@ -11,16 +11,24 @@ import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
|
||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_COMMAND_CLOSE,
|
||||
@@ -34,6 +42,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -74,6 +83,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
|
||||
): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}
|
||||
)
|
||||
COVER_SCHEMA = vol.Schema(
|
||||
@@ -86,6 +98,9 @@ COVER_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
NOTIFY_SCHEMA = vol.Schema(
|
||||
@@ -106,6 +121,9 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
@@ -118,6 +136,9 @@ SWITCH_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
COMBINED_SCHEMA = vol.Schema(
|
||||
@@ -152,7 +173,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
platforms: list[Platform] = []
|
||||
for platform_config in command_line_config:
|
||||
for platform, _config in platform_config.items():
|
||||
platforms.append(PLATFORM_MAPPING[platform])
|
||||
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
|
||||
platforms.append(mapped_platform)
|
||||
_LOGGER.debug(
|
||||
"Loading config %s for platform %s",
|
||||
platform_config,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for custom shell commands to retrieve values."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -18,17 +19,19 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .sensor import CommandSensorData
|
||||
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
@@ -84,6 +87,9 @@ async def async_setup_platform(
|
||||
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
|
||||
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
|
||||
scan_interval: timedelta = binary_sensor_config.get(
|
||||
CONF_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
@@ -98,15 +104,17 @@ async def async_setup_platform(
|
||||
payload_off,
|
||||
value_template,
|
||||
unique_id,
|
||||
scan_interval,
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class CommandBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a command line binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
@@ -116,6 +124,7 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
payload_off: str,
|
||||
value_template: Template | None,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self.data = data
|
||||
@@ -126,8 +135,39 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
self._payload_off = payload_off
|
||||
self._value_template = value_template
|
||||
self._attr_unique_id = unique_id
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._update_entity_state(None)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Binary Sensor - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
value = self.data.value
|
||||
@@ -141,3 +181,5 @@ class CommandBinarySensor(BinarySensorEntity):
|
||||
self._attr_is_on = True
|
||||
elif value == self._payload_off:
|
||||
self._attr_is_on = False
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_COMMAND_TIMEOUT = "command_timeout"
|
||||
DEFAULT_TIMEOUT = 15
|
||||
DOMAIN = "command_line"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Support for command line covers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -19,21 +20,23 @@ from homeassistant.const import (
|
||||
CONF_COVERS,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
COVER_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -97,11 +100,12 @@ async def async_setup_platform(
|
||||
value_template,
|
||||
device_config[CONF_COMMAND_TIMEOUT],
|
||||
device_config.get(CONF_UNIQUE_ID),
|
||||
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||
)
|
||||
)
|
||||
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
LOGGER.error("No covers added")
|
||||
return
|
||||
|
||||
async_add_entities(covers)
|
||||
@@ -110,6 +114,8 @@ async def async_setup_platform(
|
||||
class CommandCover(CoverEntity):
|
||||
"""Representation a command line cover."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
|
||||
value_template: Template | None,
|
||||
timeout: int,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the cover."""
|
||||
self._attr_name = name
|
||||
@@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
|
||||
self._value_template = value_template
|
||||
self._timeout = timeout
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_should_poll = bool(command_state)
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self._command_state:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Cover - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
def _move_cover(self, command: str) -> bool:
|
||||
"""Execute the actual commands."""
|
||||
_LOGGER.info("Running command: %s", command)
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
returncode = call_shell_with_timeout(command, self._timeout)
|
||||
success = returncode == 0
|
||||
|
||||
if not success:
|
||||
_LOGGER.error(
|
||||
LOGGER.error(
|
||||
"Command failed (with return code %s): %s", returncode, command
|
||||
)
|
||||
|
||||
@@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
|
||||
def _query_state(self) -> str | None:
|
||||
"""Query for the state."""
|
||||
if self._command_state:
|
||||
_LOGGER.info("Running state value command: %s", self._command_state)
|
||||
LOGGER.info("Running state value command: %s", self._command_state)
|
||||
return check_output_or_log(self._command_state, self._timeout)
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
@@ -181,15 +218,19 @@ class CommandCover(CoverEntity):
|
||||
self._state = None
|
||||
if payload:
|
||||
self._state = int(payload)
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
self._move_cover(self._command_open)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
def close_cover(self, **kwargs: Any) -> None:
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
self._move_cover(self._command_close)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self._move_cover(self._command_stop)
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
|
||||
await self._update_entity_state(None)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -28,15 +29,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_JSON_ATTRIBUTES = "json_attributes"
|
||||
|
||||
DEFAULT_NAME = "Command Sensor"
|
||||
@@ -88,6 +88,7 @@ async def async_setup_platform(
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
||||
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
async_add_entities(
|
||||
@@ -99,15 +100,17 @@ async def async_setup_platform(
|
||||
value_template,
|
||||
json_attributes,
|
||||
unique_id,
|
||||
scan_interval,
|
||||
)
|
||||
],
|
||||
True,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class CommandSensor(SensorEntity):
|
||||
"""Representation of a sensor that is using shell commands."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
@@ -116,6 +119,7 @@ class CommandSensor(SensorEntity):
|
||||
value_template: Template | None,
|
||||
json_attributes: list[str] | None,
|
||||
unique_id: str | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = name
|
||||
@@ -126,8 +130,39 @@ class CommandSensor(SensorEntity):
|
||||
self._value_template = value_template
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_unique_id = unique_id
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._update_entity_state(None)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Sensor - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
value = self.data.value
|
||||
@@ -144,11 +179,11 @@ class CommandSensor(SensorEntity):
|
||||
if k in json_dict
|
||||
}
|
||||
else:
|
||||
_LOGGER.warning("JSON result was not a dictionary")
|
||||
LOGGER.warning("JSON result was not a dictionary")
|
||||
except ValueError:
|
||||
_LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
else:
|
||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
if self._value_template is None:
|
||||
self._attr_native_value = None
|
||||
return
|
||||
@@ -163,6 +198,8 @@ class CommandSensor(SensorEntity):
|
||||
else:
|
||||
self._attr_native_value = value
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class CommandSensorData:
|
||||
"""The class for handling the data retrieval."""
|
||||
@@ -191,7 +228,7 @@ class CommandSensorData:
|
||||
args_to_render = {"arguments": args}
|
||||
rendered_args = args_compiled.render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.exception("Error rendering command template: %s", ex)
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
else:
|
||||
rendered_args = None
|
||||
@@ -203,5 +240,5 @@ class CommandSensorData:
|
||||
# Template used. Construct the string used in the shell
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
_LOGGER.debug("Running command: %s", command)
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
self.value = check_output_or_log(command, self.timeout)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Support for custom shell commands to turn a switch on/off."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -20,6 +21,7 @@ from homeassistant.const import (
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_SWITCHES,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -27,16 +29,17 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -112,11 +115,12 @@ async def async_setup_platform(
|
||||
device_config.get(CONF_COMMAND_STATE),
|
||||
value_template,
|
||||
device_config[CONF_COMMAND_TIMEOUT],
|
||||
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||
)
|
||||
)
|
||||
|
||||
if not switches:
|
||||
_LOGGER.error("No switches added")
|
||||
LOGGER.error("No switches added")
|
||||
return
|
||||
|
||||
async_add_entities(switches)
|
||||
@@ -125,6 +129,8 @@ async def async_setup_platform(
|
||||
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
"""Representation a switch that can be toggled using shell commands."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ConfigType,
|
||||
@@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(self.hass, config)
|
||||
@@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
self._command_state = command_state
|
||||
self._value_template = value_template
|
||||
self._timeout = timeout
|
||||
self._attr_should_poll = bool(command_state)
|
||||
self._scan_interval = scan_interval
|
||||
self._process_updates: asyncio.Lock | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self._command_state:
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._update_entity_state,
|
||||
self._scan_interval,
|
||||
name=f"Command Line Cover - {self.name}",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _switch(self, command: str) -> bool:
|
||||
"""Execute the actual commands."""
|
||||
_LOGGER.info("Running command: %s", command)
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
success = (
|
||||
await self.hass.async_add_executor_job(
|
||||
@@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Command failed: %s", command)
|
||||
LOGGER.error("Command failed: %s", command)
|
||||
|
||||
return success
|
||||
|
||||
def _query_state_value(self, command: str) -> str | None:
|
||||
"""Execute state command for return value."""
|
||||
_LOGGER.info("Running state value command: %s", command)
|
||||
LOGGER.info("Running state value command: %s", command)
|
||||
return check_output_or_log(command, self._timeout)
|
||||
|
||||
def _query_state_code(self, command: str) -> bool:
|
||||
"""Execute state command for return code."""
|
||||
_LOGGER.info("Running state code command: %s", command)
|
||||
LOGGER.info("Running state code command: %s", command)
|
||||
return (
|
||||
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
|
||||
)
|
||||
@@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def _update_entity_state(self, now) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
|
||||
self.name,
|
||||
self._scan_interval,
|
||||
)
|
||||
return
|
||||
|
||||
async with self._process_updates:
|
||||
await self._async_update()
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
@@ -201,15 +238,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
if payload or value:
|
||||
self._attr_is_on = (value or payload).lower() == "true"
|
||||
self._process_manual_data(payload)
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
if await self._switch(self._command_on) and not self._command_state:
|
||||
self._attr_is_on = True
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._update_entity_state(None)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
if await self._switch(self._command_off) and not self._command_state:
|
||||
self._attr_is_on = False
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._update_entity_state(None)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"]
|
||||
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"]
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class Flexit(ClimateEntity):
|
||||
result = float(
|
||||
await self._async_read_int16_from_register(register_type, register)
|
||||
)
|
||||
if result == -1:
|
||||
if not result:
|
||||
return -1
|
||||
return result / 10.0
|
||||
|
||||
@@ -200,6 +200,6 @@ class Flexit(ClimateEntity):
|
||||
result = await self._hub.async_pymodbus_call(
|
||||
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
|
||||
)
|
||||
if result == -1:
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20230601.1"]
|
||||
"requirements": ["home-assistant-frontend==20230608.0"]
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Component for the Goalfeed service."""
|
||||
import json
|
||||
|
||||
import pysher
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
# Version downgraded due to regression in library
|
||||
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
|
||||
DOMAIN = "goalfeed"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
GOALFEED_HOST = "feed.goalfeed.ca"
|
||||
GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth"
|
||||
GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Goalfeed component."""
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
def goal_handler(data):
|
||||
"""Handle goal events."""
|
||||
goal = json.loads(json.loads(data))
|
||||
|
||||
hass.bus.fire("goal", event_data=goal)
|
||||
|
||||
def connect_handler(data):
|
||||
"""Handle connection."""
|
||||
post_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"connection_info": data,
|
||||
}
|
||||
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json()
|
||||
|
||||
channel = pusher.subscribe("private-goals", resp["auth"])
|
||||
channel.bind("goal", goal_handler)
|
||||
|
||||
pusher = pysher.Pusher(
|
||||
GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST
|
||||
)
|
||||
|
||||
pusher.connection.bind("pusher:connection_established", connect_handler)
|
||||
pusher.connect()
|
||||
|
||||
return True
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "goalfeed",
|
||||
"name": "Goalfeed",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/goalfeed",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysher"],
|
||||
"requirements": ["pysher==1.0.7"]
|
||||
}
|
||||
@@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
|
||||
SOURCE_CLOUD = "cloud"
|
||||
SOURCE_LOCAL = "local"
|
||||
|
||||
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
|
||||
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT}
|
||||
|
||||
FAN_SPEEDS = {
|
||||
"5/5": ["High", "Max", "Fast", "5"],
|
||||
|
||||
@@ -7,13 +7,18 @@ import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
@@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
except RefreshError as ex:
|
||||
entry.async_start_reauth(hass)
|
||||
raise ex
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
row_data = {"created": str(datetime.now())} | call.data[DATA]
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==2.6.4"],
|
||||
"requirements": ["aiohomekit==2.6.5"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class ImapMessage:
|
||||
@property
|
||||
def subject(self) -> str:
|
||||
"""Decode the message subject."""
|
||||
decoded_header = decode_header(self.email_message["Subject"])
|
||||
decoded_header = decode_header(self.email_message["Subject"] or "")
|
||||
subject_header = make_header(decoded_header)
|
||||
return str(subject_header)
|
||||
|
||||
|
||||
@@ -302,12 +302,9 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new option."""
|
||||
if option not in self.options:
|
||||
_LOGGER.warning(
|
||||
"Invalid option: %s (possible options: %s)",
|
||||
option,
|
||||
", ".join(self.options),
|
||||
raise HomeAssistantError(
|
||||
f"Invalid option: {option} (possible options: {', '.join(self.options)})"
|
||||
)
|
||||
return
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
SENSOR_TYPES = {
|
||||
OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING,
|
||||
@@ -62,7 +62,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.BINARY_SENSOR}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
|
||||
async_add_insteon_binary_sensor_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.BINARY_SENSOR,
|
||||
InsteonBinarySensorEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
FAN_ONLY = "fan_only"
|
||||
|
||||
@@ -71,7 +71,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.CLIMATE}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
|
||||
async_add_insteon_climate_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.CLIMATE,
|
||||
InsteonClimateEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonClimateEntity(InsteonEntity, ClimateEntity):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -34,7 +34,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.COVER}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
|
||||
async_add_insteon_cover_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.COVER,
|
||||
InsteonCoverEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonCoverEntity(InsteonEntity, CoverEntity):
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.util.percentage import (
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
SPEED_RANGE = (1, 255) # off is not included
|
||||
|
||||
@@ -38,7 +38,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.FAN}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
|
||||
async_add_insteon_fan_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.FAN,
|
||||
InsteonFanEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonFanEntity(InsteonEntity, FanEntity):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Utility methods for the Insteon platform."""
|
||||
from collections.abc import Iterable
|
||||
|
||||
from pyinsteon.device_types.device_base import Device
|
||||
from pyinsteon.device_types.ipdb import (
|
||||
AccessControl_Morningstar,
|
||||
ClimateControl_Thermostat,
|
||||
@@ -44,7 +47,7 @@ from pyinsteon.device_types.ipdb import (
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DEVICE_PLATFORM = {
|
||||
DEVICE_PLATFORM: dict[Device, dict[Platform, Iterable[int]]] = {
|
||||
AccessControl_Morningstar: {Platform.LOCK: [1]},
|
||||
DimmableLightingControl: {Platform.LIGHT: [1]},
|
||||
DimmableLightingControl_Dial: {Platform.LIGHT: [1]},
|
||||
@@ -101,11 +104,11 @@ DEVICE_PLATFORM = {
|
||||
}
|
||||
|
||||
|
||||
def get_device_platforms(device):
|
||||
def get_device_platforms(device) -> dict[Platform, Iterable[int]]:
|
||||
"""Return the HA platforms for a device type."""
|
||||
return DEVICE_PLATFORM.get(type(device), {}).keys()
|
||||
return DEVICE_PLATFORM.get(type(device), {})
|
||||
|
||||
|
||||
def get_platform_groups(device, domain) -> dict:
|
||||
"""Return the platforms that a device belongs in."""
|
||||
return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined]
|
||||
def get_device_platform_groups(device: Device, platform: Platform) -> Iterable[int]:
|
||||
"""Return the list of device groups for a platform."""
|
||||
return get_device_platforms(device).get(platform, [])
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
MAX_BRIGHTNESS = 255
|
||||
|
||||
@@ -37,7 +37,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LIGHT}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
|
||||
async_add_insteon_light_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.LIGHT,
|
||||
InsteonDimmerEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonDimmerEntity(InsteonEntity, LightEntity):
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -30,7 +30,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LOCK}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
|
||||
async_add_insteon_lock_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.LOCK,
|
||||
InsteonLockEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonLockEntity(InsteonEntity, LockEntity):
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
from .utils import async_add_insteon_devices, async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -33,7 +33,12 @@ async def async_setup_entry(
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.SWITCH}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
|
||||
async_add_insteon_switch_entities()
|
||||
async_add_insteon_devices(
|
||||
hass,
|
||||
Platform.SWITCH,
|
||||
InsteonSwitchEntity,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Utilities used by insteon component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyinsteon import devices
|
||||
from pyinsteon.address import Address
|
||||
@@ -30,6 +33,7 @@ from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
ENTITY_MATCH_ALL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -38,6 +42,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_CAT,
|
||||
@@ -78,7 +83,7 @@ from .const import (
|
||||
SRV_X10_ALL_LIGHTS_ON,
|
||||
SRV_X10_ALL_UNITS_OFF,
|
||||
)
|
||||
from .ipdb import get_device_platforms, get_platform_groups
|
||||
from .ipdb import get_device_platform_groups, get_device_platforms
|
||||
from .schemas import (
|
||||
ADD_ALL_LINK_SCHEMA,
|
||||
ADD_DEFAULT_LINKS_SCHEMA,
|
||||
@@ -89,6 +94,9 @@ from .schemas import (
|
||||
X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .insteon_entity import InsteonEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -132,6 +140,9 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
||||
_LOGGER.debug("Firing event %s with %s", event, schema)
|
||||
hass.bus.async_fire(event, schema)
|
||||
|
||||
if str(device.address).startswith("X10"):
|
||||
return
|
||||
|
||||
for name_or_group, event in device.events.items():
|
||||
if isinstance(name_or_group, int):
|
||||
for _, event in device.events[name_or_group].items():
|
||||
@@ -158,8 +169,10 @@ def register_new_device_callback(hass):
|
||||
await device.async_status()
|
||||
platforms = get_device_platforms(device)
|
||||
for platform in platforms:
|
||||
groups = get_device_platform_groups(device, platform)
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
|
||||
dispatcher_send(hass, signal, {"address": device.address})
|
||||
dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
|
||||
add_insteon_events(hass, device)
|
||||
|
||||
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
|
||||
|
||||
@@ -383,20 +396,38 @@ def print_aldb_to_log(aldb):
|
||||
|
||||
@callback
|
||||
def async_add_insteon_entities(
|
||||
hass, platform, entity_type, async_add_entities, discovery_info
|
||||
):
|
||||
"""Add Insteon devices to a platform."""
|
||||
new_entities = []
|
||||
device_list = [discovery_info.get("address")] if discovery_info else devices
|
||||
|
||||
for address in device_list:
|
||||
device = devices[address]
|
||||
groups = get_platform_groups(device, platform)
|
||||
for group in groups:
|
||||
new_entities.append(entity_type(device, group))
|
||||
hass: HomeAssistant,
|
||||
platform: Platform,
|
||||
entity_type: type[InsteonEntity],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: dict[str, Any],
|
||||
) -> None:
|
||||
"""Add an Insteon group to a platform."""
|
||||
address = discovery_info["address"]
|
||||
device = devices[address]
|
||||
new_entities = [
|
||||
entity_type(device=device, group=group) for group in discovery_info["groups"]
|
||||
]
|
||||
async_add_entities(new_entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_insteon_devices(
|
||||
hass: HomeAssistant,
|
||||
platform: Platform,
|
||||
entity_type: type[InsteonEntity],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add all entities to a platform."""
|
||||
for address in devices:
|
||||
device = devices[address]
|
||||
groups = get_device_platform_groups(device, platform)
|
||||
discovery_info = {"address": address, "groups": groups}
|
||||
async_add_insteon_entities(
|
||||
hass, platform, entity_type, async_add_entities, discovery_info
|
||||
)
|
||||
|
||||
|
||||
def get_usb_ports() -> dict[str, str]:
|
||||
"""Return a dict of USB ports and their friendly names."""
|
||||
ports = list_ports.comports()
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pylast import LastFMNetwork, User, WSError
|
||||
from pylast import LastFMNetwork, PyLastError, User, WSError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -128,11 +128,14 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
main_user, _ = get_lastfm_user(
|
||||
self.data[CONF_API_KEY], self.data[CONF_MAIN_USER]
|
||||
)
|
||||
friends_response = await self.hass.async_add_executor_job(
|
||||
main_user.get_friends
|
||||
)
|
||||
friends = [
|
||||
SelectOptionDict(value=friend.name, label=friend.get_name(True))
|
||||
for friend in main_user.get_friends()
|
||||
for friend in friends_response
|
||||
]
|
||||
except WSError:
|
||||
except PyLastError:
|
||||
friends = []
|
||||
return self.async_show_form(
|
||||
step_id="friends",
|
||||
@@ -197,11 +200,14 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
self.options[CONF_API_KEY],
|
||||
self.options[CONF_MAIN_USER],
|
||||
)
|
||||
friends_response = await self.hass.async_add_executor_job(
|
||||
main_user.get_friends
|
||||
)
|
||||
friends = [
|
||||
SelectOptionDict(value=friend.name, label=friend.get_name(True))
|
||||
for friend in main_user.get_friends()
|
||||
for friend in friends_response
|
||||
]
|
||||
except WSError:
|
||||
except PyLastError:
|
||||
friends = []
|
||||
else:
|
||||
friends = []
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from . import websocket_api
|
||||
from .const import (
|
||||
ATTR_LEVEL,
|
||||
DEFAULT_LOGSEVERITY,
|
||||
DOMAIN,
|
||||
LOGGER_DEFAULT,
|
||||
LOGGER_FILTERS,
|
||||
@@ -39,9 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY
|
||||
): _VALID_LOG_LEVEL,
|
||||
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
|
||||
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
|
||||
vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class LoggerSettings:
|
||||
|
||||
self._yaml_config = yaml_config
|
||||
self._default_level = logging.INFO
|
||||
if DOMAIN in yaml_config:
|
||||
if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
|
||||
self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
|
||||
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
|
||||
hass, STORAGE_VERSION, STORAGE_KEY
|
||||
|
||||
@@ -38,7 +38,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
class LocalSource(MediaSource):
|
||||
"""Provide local directories as media sources."""
|
||||
|
||||
name: str = "Local Media"
|
||||
name: str = "My media"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize local source."""
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/melnor",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["melnor-bluetooth==0.0.22"]
|
||||
"requirements": ["melnor-bluetooth==0.0.25"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nuki",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynuki"],
|
||||
"requirements": ["pynuki==1.6.1"]
|
||||
"requirements": ["pynuki==1.6.2"]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@joostlek"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/opensky",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-opensky==0.0.7"]
|
||||
"requirements": ["python-opensky==0.0.9"]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ def setup_platform(
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
radius = config.get(CONF_RADIUS, 0)
|
||||
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius)
|
||||
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
|
||||
session = async_get_clientsession(hass)
|
||||
opensky = OpenSky(session=session)
|
||||
add_entities(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.1.0"]
|
||||
"requirements": ["python-otbr-api==2.2.0"]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ class OTBRData:
|
||||
"""Create an active operational dataset."""
|
||||
return await self.api.create_active_dataset(dataset)
|
||||
|
||||
@_handle_otbr_error
|
||||
async def delete_active_dataset(self) -> None:
|
||||
"""Delete the active operational dataset."""
|
||||
return await self.api.delete_active_dataset()
|
||||
|
||||
@_handle_otbr_error
|
||||
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
|
||||
"""Set current active operational dataset in TLVS format."""
|
||||
|
||||
@@ -81,6 +81,12 @@ async def websocket_create_network(
|
||||
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
|
||||
return
|
||||
|
||||
try:
|
||||
await data.delete_active_dataset()
|
||||
except HomeAssistantError as exc:
|
||||
connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc))
|
||||
return
|
||||
|
||||
try:
|
||||
await data.create_active_dataset(
|
||||
python_otbr_api.ActiveDataSet(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.7.9"],
|
||||
"requirements": ["pyoverkiz==1.8.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id"
|
||||
ATTR_TITLE: Final = "title"
|
||||
ATTR_STATUS: Final = "status"
|
||||
|
||||
STATUS_UNREAD = "unread"
|
||||
STATUS_READ = "read"
|
||||
|
||||
# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9
|
||||
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated"
|
||||
@@ -43,7 +41,6 @@ class Notification(TypedDict):
|
||||
message: str
|
||||
notification_id: str
|
||||
title: str | None
|
||||
status: str
|
||||
|
||||
|
||||
class UpdateType(StrEnum):
|
||||
@@ -98,7 +95,6 @@ def async_create(
|
||||
notifications[notification_id] = {
|
||||
ATTR_MESSAGE: message,
|
||||
ATTR_NOTIFICATION_ID: notification_id,
|
||||
ATTR_STATUS: STATUS_UNREAD,
|
||||
ATTR_TITLE: title,
|
||||
ATTR_CREATED_AT: dt_util.utcnow(),
|
||||
}
|
||||
@@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the persistent notification component."""
|
||||
notifications = _async_get_or_create_notifications(hass)
|
||||
|
||||
@callback
|
||||
def create_service(call: ServiceCall) -> None:
|
||||
@@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Handle the dismiss notification service call."""
|
||||
async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID])
|
||||
|
||||
@callback
|
||||
def mark_read_service(call: ServiceCall) -> None:
|
||||
"""Handle the mark_read notification service call."""
|
||||
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
|
||||
if notification_id not in notifications:
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Marking persistent_notification read failed: "
|
||||
"Notification ID %s not found"
|
||||
),
|
||||
notification_id,
|
||||
)
|
||||
return
|
||||
|
||||
notification = notifications[notification_id]
|
||||
notification[ATTR_STATUS] = STATUS_READ
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
|
||||
UpdateType.UPDATED,
|
||||
{notification_id: notification},
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"create",
|
||||
@@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_get_notifications)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_notifications)
|
||||
|
||||
|
||||
@@ -33,15 +33,3 @@ dismiss:
|
||||
example: 1234
|
||||
selector:
|
||||
text:
|
||||
|
||||
mark_read:
|
||||
name: Mark read
|
||||
description: Mark a notification read.
|
||||
fields:
|
||||
notification_id:
|
||||
name: Notification ID
|
||||
description: Target ID of the notification, which should be mark read.
|
||||
required: true
|
||||
example: 1234
|
||||
selector:
|
||||
text:
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pulsectl==20.2.4"]
|
||||
"requirements": ["pulsectl==23.5.2"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Support for the Airzone diagnostics."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
|
||||
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
|
||||
|
||||
TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
|
||||
"coordinators": {
|
||||
f"**REDACTED-{i}**": {
|
||||
"roborock_device_info": async_redact_data(
|
||||
coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD
|
||||
),
|
||||
"api": coordinator.api.diagnostic_data,
|
||||
}
|
||||
for i, coordinator in enumerate(coordinators.values())
|
||||
},
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": ["python-roborock==0.17.0"]
|
||||
"requirements": ["python-roborock==0.23.4"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Roborock Models."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
|
||||
from roborock.roborock_typing import DeviceProp
|
||||
@@ -13,3 +14,12 @@ class RoborockHassDeviceInfo:
|
||||
network_info: NetworkInfo
|
||||
product: HomeDataProduct
|
||||
props: DeviceProp
|
||||
|
||||
def as_dict(self) -> dict[str, dict[str, Any]]:
|
||||
"""Turn RoborockHassDeviceInfo into a dictionary."""
|
||||
return {
|
||||
"device": self.device.as_dict(),
|
||||
"network_info": self.network_info.as_dict(),
|
||||
"product": self.product.as_dict(),
|
||||
"props": self.props.as_dict(),
|
||||
}
|
||||
|
||||
@@ -90,8 +90,11 @@
|
||||
"name": "Mop intensity",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"low": "Low",
|
||||
"mild": "Mild",
|
||||
"medium": "Medium",
|
||||
"moderate": "Moderate",
|
||||
"high": "High",
|
||||
"intense": "Intense",
|
||||
"custom": "Custom"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Support for Roborock switch."""
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
@@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin:
|
||||
evaluate_value: Callable[[dict], bool]
|
||||
# Sets the status of the switch
|
||||
set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]]
|
||||
# Check support of this feature
|
||||
check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
|
||||
RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0}
|
||||
),
|
||||
get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS),
|
||||
check_support=lambda data: data.api.send_command(
|
||||
RoborockCommand.GET_CHILD_LOCK_STATUS
|
||||
),
|
||||
evaluate_value=lambda data: data["lock_status"] == 1,
|
||||
key="child_lock",
|
||||
translation_key="child_lock",
|
||||
@@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
|
||||
RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0}
|
||||
),
|
||||
get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS),
|
||||
check_support=lambda data: data.api.send_command(
|
||||
RoborockCommand.GET_FLOW_LED_STATUS
|
||||
),
|
||||
evaluate_value=lambda data: data["status"] == 1,
|
||||
key="status_indicator",
|
||||
translation_key="status_indicator",
|
||||
@@ -75,16 +85,38 @@ async def async_setup_entry(
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
async_add_entities(
|
||||
(
|
||||
RoborockSwitchEntity(
|
||||
f"{description.key}_{slugify(device_id)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
possible_entities: list[
|
||||
tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription]
|
||||
] = [
|
||||
(device_id, coordinator, description)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
]
|
||||
# We need to check if this function is supported by the device.
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
description.check_support(coordinator)
|
||||
for _, coordinator, description in possible_entities
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
valid_entities: list[RoborockSwitchEntity] = []
|
||||
for posible_entity, result in zip(possible_entities, results):
|
||||
if isinstance(result, Exception):
|
||||
if not isinstance(result, RoborockException):
|
||||
raise result
|
||||
_LOGGER.debug("Not adding entity because of %s", result)
|
||||
else:
|
||||
valid_entities.append(
|
||||
RoborockSwitchEntity(
|
||||
f"{posible_entity[2].key}_{slugify(posible_entity[0])}",
|
||||
posible_entity[1],
|
||||
posible_entity[2],
|
||||
result,
|
||||
)
|
||||
)
|
||||
async_add_entities(
|
||||
valid_entities,
|
||||
True,
|
||||
)
|
||||
|
||||
@@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity):
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
entity_description: RoborockSwitchDescription,
|
||||
initial_value: bool,
|
||||
) -> None:
|
||||
"""Create a switch entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(unique_id, coordinator.device_info, coordinator.api)
|
||||
self._attr_is_on = initial_value
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"],
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
|
||||
last_stats = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, statistic_id, True, {}
|
||||
get_last_statistics, self.hass, 1, statistic_id, True, set()
|
||||
)
|
||||
|
||||
if not last_stats:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==47"],
|
||||
"requirements": ["aiounifi==48"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -14,8 +14,6 @@ from pyunifiprotect.data import (
|
||||
ProtectAdoptableDeviceModel,
|
||||
ProtectModelWithId,
|
||||
Sensor,
|
||||
SmartDetectAudioType,
|
||||
SmartDetectObjectType,
|
||||
)
|
||||
from pyunifiprotect.data.nvr import UOSDisk
|
||||
|
||||
@@ -364,8 +362,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_person",
|
||||
ufp_enabled="is_person_detection_on",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
ufp_smart_type=SmartDetectObjectType.PERSON,
|
||||
ufp_event_obj="last_person_detect_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_obj_vehicle",
|
||||
@@ -374,8 +371,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_vehicle",
|
||||
ufp_enabled="is_vehicle_detection_on",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
ufp_smart_type=SmartDetectObjectType.VEHICLE,
|
||||
ufp_event_obj="last_vehicle_detect_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_obj_face",
|
||||
@@ -384,8 +380,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_face",
|
||||
ufp_enabled="is_face_detection_on",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
ufp_smart_type=SmartDetectObjectType.FACE,
|
||||
ufp_event_obj="last_face_detect_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_obj_package",
|
||||
@@ -394,8 +389,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_package",
|
||||
ufp_enabled="is_package_detection_on",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
ufp_smart_type=SmartDetectObjectType.PACKAGE,
|
||||
ufp_event_obj="last_package_detect_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_audio_any",
|
||||
@@ -412,8 +406,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_smoke",
|
||||
ufp_enabled="is_smoke_detection_on",
|
||||
ufp_event_obj="last_smart_audio_detect_event",
|
||||
ufp_smart_type=SmartDetectAudioType.SMOKE,
|
||||
ufp_event_obj="last_smoke_detect_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_audio_cmonx",
|
||||
@@ -422,8 +415,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_smoke",
|
||||
ufp_enabled="is_smoke_detection_on",
|
||||
ufp_event_obj="last_smart_audio_detect_event",
|
||||
ufp_smart_type=SmartDetectAudioType.CMONX,
|
||||
ufp_event_obj="last_cmonx_detect_event",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
|
||||
SMART_EVENTS = {
|
||||
EventType.SMART_DETECT,
|
||||
EventType.SMART_AUDIO_DETECT,
|
||||
EventType.SMART_DETECT_LINE,
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
@@ -223,6 +228,25 @@ class ProtectData:
|
||||
|
||||
# trigger updates for camera that the event references
|
||||
elif isinstance(obj, Event):
|
||||
if obj.type in SMART_EVENTS:
|
||||
if obj.camera is not None:
|
||||
if obj.end is None:
|
||||
_LOGGER.debug(
|
||||
"%s (%s): New smart detection started for %s (%s)",
|
||||
obj.camera.name,
|
||||
obj.camera.mac,
|
||||
obj.smart_detect_types,
|
||||
obj.id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s (%s): Smart detection ended for %s (%s)",
|
||||
obj.camera.name,
|
||||
obj.camera.mac,
|
||||
obj.smart_detect_types,
|
||||
obj.id,
|
||||
)
|
||||
|
||||
if obj.type == EventType.DEVICE_ADOPTED:
|
||||
if obj.metadata is not None and obj.metadata.device_id is not None:
|
||||
device = self.api.bootstrap.get_device_from_id(
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyunifiprotect==4.9.1", "unifi-discovery==1.1.7"],
|
||||
"requirements": ["pyunifiprotect==4.10.2", "unifi-discovery==1.1.7"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import Any, Generic, TypeVar, cast
|
||||
@@ -10,6 +11,7 @@ from typing import Any, Generic, TypeVar, cast
|
||||
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
|
||||
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .utils import get_nested_attr
|
||||
|
||||
@@ -67,7 +69,6 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
|
||||
"""Mixin for events."""
|
||||
|
||||
ufp_event_obj: str | None = None
|
||||
ufp_smart_type: str | None = None
|
||||
|
||||
def get_event_obj(self, obj: T) -> Event | None:
|
||||
"""Return value from UniFi Protect device."""
|
||||
@@ -79,23 +80,22 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
|
||||
def get_is_on(self, obj: T) -> bool:
|
||||
"""Return value if event is active."""
|
||||
|
||||
value = bool(self.get_ufp_value(obj))
|
||||
if value:
|
||||
event = self.get_event_obj(obj)
|
||||
value = event is not None
|
||||
if not value:
|
||||
_LOGGER.debug("%s (%s): missing event", self.name, obj.mac)
|
||||
event = self.get_event_obj(obj)
|
||||
if event is None:
|
||||
return False
|
||||
|
||||
if event is not None and self.ufp_smart_type is not None:
|
||||
value = self.ufp_smart_type in event.smart_detect_types
|
||||
if not value:
|
||||
_LOGGER.debug(
|
||||
"%s (%s): %s not in %s",
|
||||
self.name,
|
||||
obj.mac,
|
||||
self.ufp_smart_type,
|
||||
event.smart_detect_types,
|
||||
)
|
||||
now = dt_util.utcnow()
|
||||
value = now > event.start
|
||||
if value and event.end is not None and now > event.end:
|
||||
value = False
|
||||
# only log if the recent ended recently
|
||||
if event.end + timedelta(seconds=10) < now:
|
||||
_LOGGER.debug(
|
||||
"%s (%s): end ended at %s",
|
||||
self.name,
|
||||
obj.mac,
|
||||
event.end.isoformat(),
|
||||
)
|
||||
|
||||
if value:
|
||||
_LOGGER.debug("%s (%s): value is on", self.name, obj.mac)
|
||||
|
||||
@@ -15,7 +15,6 @@ from pyunifiprotect.data import (
|
||||
ProtectDeviceModel,
|
||||
ProtectModelWithId,
|
||||
Sensor,
|
||||
SmartDetectObjectType,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -528,10 +527,9 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
|
||||
name="License Plate Detected",
|
||||
icon="mdi:car",
|
||||
translation_key="license_plate",
|
||||
ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE,
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_license_plate",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
ufp_event_obj="last_license_plate_detect_event",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -767,8 +765,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
|
||||
EventEntityMixin._async_update_device_from_protect(self, device)
|
||||
is_on = self.entity_description.get_is_on(device)
|
||||
is_license_plate = (
|
||||
self.entity_description.ufp_smart_type
|
||||
== SmartDetectObjectType.LICENSE_PLATE
|
||||
self.entity_description.ufp_event_obj == "last_license_plate_detect_event"
|
||||
)
|
||||
if (
|
||||
not is_on
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/waqi",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["waqiasync"],
|
||||
"requirements": ["waqiasync==1.0.0"]
|
||||
"requirements": ["waqiasync==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["xiaomi-ble==0.17.0"]
|
||||
"requirements": ["xiaomi-ble==0.17.2"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Entity representing a YouTube account."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -21,20 +18,18 @@ class YouTubeChannelEntity(CoordinatorEntity):
|
||||
self,
|
||||
coordinator: YouTubeDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
channel: dict[str, Any],
|
||||
channel_id: str,
|
||||
) -> None:
|
||||
"""Initialize a Google Mail entity."""
|
||||
"""Initialize a YouTube entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}_{description.key}"
|
||||
f"{coordinator.config_entry.entry_id}_{channel_id}_{description.key}"
|
||||
)
|
||||
self._channel_id = channel_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={
|
||||
(DOMAIN, f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}")
|
||||
},
|
||||
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{channel_id}")},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=channel[ATTR_TITLE],
|
||||
name=coordinator.data[channel_id][ATTR_TITLE],
|
||||
)
|
||||
self._channel = channel
|
||||
|
||||
@@ -18,6 +18,7 @@ from .const import (
|
||||
ATTR_SUBSCRIBER_COUNT,
|
||||
ATTR_THUMBNAIL,
|
||||
ATTR_TITLE,
|
||||
ATTR_VIDEO_ID,
|
||||
COORDINATOR,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -30,6 +31,7 @@ class YouTubeMixin:
|
||||
|
||||
value_fn: Callable[[Any], StateType]
|
||||
entity_picture_fn: Callable[[Any], str]
|
||||
attributes_fn: Callable[[Any], dict[str, Any]] | None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -44,6 +46,9 @@ SENSOR_TYPES = [
|
||||
icon="mdi:youtube",
|
||||
value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE],
|
||||
entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL],
|
||||
attributes_fn=lambda channel: {
|
||||
ATTR_VIDEO_ID: channel[ATTR_LATEST_VIDEO][ATTR_VIDEO_ID]
|
||||
},
|
||||
),
|
||||
YouTubeSensorEntityDescription(
|
||||
key="subscribers",
|
||||
@@ -52,6 +57,7 @@ SENSOR_TYPES = [
|
||||
native_unit_of_measurement="subscribers",
|
||||
value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT],
|
||||
entity_picture_fn=lambda channel: channel[ATTR_ICON],
|
||||
attributes_fn=None,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -64,8 +70,8 @@ async def async_setup_entry(
|
||||
COORDINATOR
|
||||
]
|
||||
async_add_entities(
|
||||
YouTubeSensor(coordinator, sensor_type, channel)
|
||||
for channel in coordinator.data.values()
|
||||
YouTubeSensor(coordinator, sensor_type, channel_id)
|
||||
for channel_id in coordinator.data
|
||||
for sensor_type in SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -78,9 +84,20 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.entity_description.value_fn(self._channel)
|
||||
return self.entity_description.value_fn(self.coordinator.data[self._channel_id])
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.entity_description.entity_picture_fn(self._channel)
|
||||
return self.entity_description.entity_picture_fn(
|
||||
self.coordinator.data[self._channel_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the extra state attributes."""
|
||||
if self.entity_description.attributes_fn:
|
||||
return self.entity_description.attributes_fn(
|
||||
self.coordinator.data[self._channel_id]
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.63.0"]
|
||||
"requirements": ["zeroconf==0.64.0"]
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_REQUEST_RETRIES = 3
|
||||
|
||||
|
||||
class AttrReportConfig(TypedDict, total=True):
|
||||
"""Configuration to report for the attributes."""
|
||||
@@ -80,8 +78,6 @@ def decorate_command(cluster_handler, command):
|
||||
|
||||
@wraps(command)
|
||||
async def wrapper(*args, **kwds):
|
||||
kwds.setdefault("tries", DEFAULT_REQUEST_RETRIES)
|
||||
|
||||
try:
|
||||
result = await command(*args, **kwds)
|
||||
cluster_handler.debug(
|
||||
|
||||
@@ -722,7 +722,9 @@ class PPBVOCLevel(Sensor):
|
||||
"""VOC Level sensor."""
|
||||
|
||||
SENSOR_ATTR = "measured_value"
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
_attr_device_class: SensorDeviceClass = (
|
||||
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_attr_name: str = "VOC level"
|
||||
_decimals = 0
|
||||
@@ -736,6 +738,7 @@ class PM25(Sensor):
|
||||
"""Particulate Matter 2.5 microns or less sensor."""
|
||||
|
||||
SENSOR_ATTR = "measured_value"
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.PM25
|
||||
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_attr_name: str = "Particulate matter"
|
||||
_decimals = 0
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeVar, cast
|
||||
|
||||
import voluptuous as vol
|
||||
import zigpy.backups
|
||||
from zigpy.config import CONF_DEVICE
|
||||
from zigpy.config.validators import cv_boolean
|
||||
from zigpy.types.named import EUI64
|
||||
from zigpy.zcl.clusters.security import IasAce
|
||||
@@ -1136,6 +1137,7 @@ async def websocket_get_network_settings(
|
||||
msg[ID],
|
||||
{
|
||||
"radio_type": async_get_radio_type(hass, zha_gateway.config_entry).name,
|
||||
"device": zha_gateway.application_controller.config[CONF_DEVICE],
|
||||
"settings": backup.as_dict(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
UPDATE_DELAY_STRING = "delay"
|
||||
UPDATE_DELAY_INTERVAL = 5 # In minutes
|
||||
ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -53,7 +54,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the extra data."""
|
||||
return {
|
||||
"latest_version_firmware": asdict(self.latest_version_firmware)
|
||||
ATTR_LATEST_VERSION_FIRMWARE: asdict(self.latest_version_firmware)
|
||||
if self.latest_version_firmware
|
||||
else None
|
||||
}
|
||||
@@ -61,7 +62,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData:
|
||||
"""Initialize the extra data from a dict."""
|
||||
if not (firmware_dict := data["latest_version_firmware"]):
|
||||
if not (firmware_dict := data[ATTR_LATEST_VERSION_FIRMWARE]):
|
||||
return cls(None)
|
||||
|
||||
return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict))
|
||||
@@ -326,20 +327,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
)
|
||||
|
||||
# If we have a complete previous state, use that to set the latest version
|
||||
if (state := await self.async_get_last_state()) and (
|
||||
extra_data := await self.async_get_last_extra_data()
|
||||
if (
|
||||
(state := await self.async_get_last_state())
|
||||
and (latest_version := state.attributes.get(ATTR_LATEST_VERSION))
|
||||
is not None
|
||||
and (extra_data := await self.async_get_last_extra_data())
|
||||
):
|
||||
self._attr_latest_version = state.attributes[ATTR_LATEST_VERSION]
|
||||
self._attr_latest_version = latest_version
|
||||
self._latest_version_firmware = (
|
||||
ZWaveNodeFirmwareUpdateExtraStoredData.from_dict(
|
||||
extra_data.as_dict()
|
||||
).latest_version_firmware
|
||||
)
|
||||
# If we have no state to restore, we can set the latest version to installed
|
||||
# so that the entity starts as off. If we have partial restore data due to an
|
||||
# upgrade to an HA version where this feature is released from one that is not
|
||||
# the entity will start in an unknown state until we can correct on next update
|
||||
elif not state:
|
||||
# If we have no state or latest version to restore, we can set the latest
|
||||
# version to installed so that the entity starts as off. If we have partial
|
||||
# restore data due to an upgrade to an HA version where this feature is released
|
||||
# from one that is not the entity will start in an unknown state until we can
|
||||
# correct on next update
|
||||
elif not state or not latest_version:
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
|
||||
# Spread updates out in 5 minute increments to avoid flooding the network
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "0b3"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
||||
@@ -1995,12 +1995,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"goalfeed": {
|
||||
"name": "Goalfeed",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"goalzero": {
|
||||
"name": "Goal Zero Yeti",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -25,8 +25,8 @@ ha-av==10.1.0
|
||||
hass-nabucasa==0.67.1
|
||||
hassil==1.0.6
|
||||
home-assistant-bluetooth==1.10.0
|
||||
home-assistant-frontend==20230601.1
|
||||
home-assistant-intents==2023.5.30
|
||||
home-assistant-frontend==20230608.0
|
||||
home-assistant-intents==2023.6.5
|
||||
httpx==0.24.1
|
||||
ifaddr==0.2.0
|
||||
janus==1.0.0
|
||||
@@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous==0.13.1
|
||||
webrtcvad==2.0.10
|
||||
yarl==1.9.2
|
||||
zeroconf==0.63.0
|
||||
zeroconf==0.64.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
@@ -128,9 +128,8 @@ authlib<1.0
|
||||
# Version 2.0 added typing, prevent accidental fallbacks
|
||||
backoff>=2.0
|
||||
|
||||
# Breaking change in version
|
||||
# https://github.com/samuelcolvin/pydantic/issues/4092
|
||||
pydantic!=1.9.1
|
||||
# Require to avoid issues with decorators (#93904). v2 has breaking changes.
|
||||
pydantic>=1.10.8,<2.0
|
||||
|
||||
# Breaks asyncio
|
||||
# https://github.com/pubnub/python/issues/130
|
||||
|
||||
@@ -245,7 +245,10 @@ async def _async_setup_component(
|
||||
severity=IssueSeverity.ERROR,
|
||||
issue_domain=domain,
|
||||
translation_key="integration_key_no_support",
|
||||
translation_placeholders={"domain": domain},
|
||||
translation_placeholders={
|
||||
"domain": domain,
|
||||
"add_integration": f"/config/integrations/dashboard/add?domain={domain}",
|
||||
},
|
||||
)
|
||||
|
||||
start = timer()
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.6.0b3"
|
||||
version = "2023.6.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
+18
-21
@@ -116,10 +116,10 @@ aio_georss_gdacs==0.8
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.1.7
|
||||
aioairzone-cloud==0.1.8
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.6.1
|
||||
aioairzone==0.6.3
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
@@ -134,7 +134,7 @@ aioasuswrt==1.4.0
|
||||
aioazuredevops==1.3.5
|
||||
|
||||
# homeassistant.components.baf
|
||||
aiobafi6==0.8.0
|
||||
aiobafi6==0.8.2
|
||||
|
||||
# homeassistant.components.aws
|
||||
aiobotocore==2.1.0
|
||||
@@ -177,7 +177,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.6.4
|
||||
aiohomekit==2.6.5
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -294,7 +294,7 @@ aiosyncthing==0.5.1
|
||||
aiotractive==0.5.5
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==47
|
||||
aiounifi==48
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -924,10 +924,10 @@ hole==0.8.0
|
||||
holidays==0.21.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230601.1
|
||||
home-assistant-frontend==20230608.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.5.30
|
||||
home-assistant-intents==2023.6.5
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -1122,7 +1122,7 @@ mcstatus==6.0.0
|
||||
meater-python==0.0.8
|
||||
|
||||
# homeassistant.components.melnor
|
||||
melnor-bluetooth==0.0.22
|
||||
melnor-bluetooth==0.0.25
|
||||
|
||||
# homeassistant.components.message_bird
|
||||
messagebird==1.2.0
|
||||
@@ -1418,7 +1418,7 @@ psutil-home-assistant==0.0.1
|
||||
psutil==5.9.5
|
||||
|
||||
# homeassistant.components.pulseaudio_loopback
|
||||
pulsectl==20.2.4
|
||||
pulsectl==23.5.2
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
pure-python-adb[async]==0.3.0.dev0
|
||||
@@ -1831,7 +1831,7 @@ pynina==0.3.0
|
||||
pynobo==1.6.0
|
||||
|
||||
# homeassistant.components.nuki
|
||||
pynuki==1.6.1
|
||||
pynuki==1.6.2
|
||||
|
||||
# homeassistant.components.nut
|
||||
pynut2==2.1.2
|
||||
@@ -1872,7 +1872,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.7.9
|
||||
pyoverkiz==1.8.0
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@@ -1966,9 +1966,6 @@ pyserial==3.5
|
||||
# homeassistant.components.sesame
|
||||
pysesame2==1.0.1
|
||||
|
||||
# homeassistant.components.goalfeed
|
||||
pysher==1.0.7
|
||||
|
||||
# homeassistant.components.sia
|
||||
pysiaalarm==3.1.1
|
||||
|
||||
@@ -2105,11 +2102,11 @@ python-mystrom==2.2.0
|
||||
python-nest==4.2.0
|
||||
|
||||
# homeassistant.components.opensky
|
||||
python-opensky==0.0.7
|
||||
python-opensky==0.0.9
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.1.0
|
||||
python-otbr-api==2.2.0
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api==1.1.0
|
||||
@@ -2121,7 +2118,7 @@ python-qbittorrent==0.4.2
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.17.0
|
||||
python-roborock==0.23.4
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@@ -2171,7 +2168,7 @@ pytrafikverket==0.3.3
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.9.1
|
||||
pyunifiprotect==4.10.2
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@@ -2635,7 +2632,7 @@ wakeonlan==2.1.0
|
||||
wallbox==0.4.12
|
||||
|
||||
# homeassistant.components.waqi
|
||||
waqiasync==1.0.0
|
||||
waqiasync==1.1.0
|
||||
|
||||
# homeassistant.components.folder_watcher
|
||||
watchdog==2.3.1
|
||||
@@ -2677,7 +2674,7 @@ wyoming==0.0.1
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.17.0
|
||||
xiaomi-ble==0.17.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.10.0
|
||||
@@ -2728,7 +2725,7 @@ zamg==0.2.2
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.63.0
|
||||
zeroconf==0.64.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
|
||||
+15
-15
@@ -106,10 +106,10 @@ aio_georss_gdacs==0.8
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.1.7
|
||||
aioairzone-cloud==0.1.8
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.6.1
|
||||
aioairzone==0.6.3
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2023.04.0
|
||||
@@ -124,7 +124,7 @@ aioasuswrt==1.4.0
|
||||
aioazuredevops==1.3.5
|
||||
|
||||
# homeassistant.components.baf
|
||||
aiobafi6==0.8.0
|
||||
aiobafi6==0.8.2
|
||||
|
||||
# homeassistant.components.aws
|
||||
aiobotocore==2.1.0
|
||||
@@ -164,7 +164,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.6.4
|
||||
aiohomekit==2.6.5
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -275,7 +275,7 @@ aiosyncthing==0.5.1
|
||||
aiotractive==0.5.5
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==47
|
||||
aiounifi==48
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.1.0
|
||||
@@ -716,10 +716,10 @@ hole==0.8.0
|
||||
holidays==0.21.13
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230601.1
|
||||
home-assistant-frontend==20230608.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.5.30
|
||||
home-assistant-intents==2023.6.5
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -848,7 +848,7 @@ mcstatus==6.0.0
|
||||
meater-python==0.0.8
|
||||
|
||||
# homeassistant.components.melnor
|
||||
melnor-bluetooth==0.0.22
|
||||
melnor-bluetooth==0.0.25
|
||||
|
||||
# homeassistant.components.meteo_france
|
||||
meteofrance-api==1.2.0
|
||||
@@ -1347,7 +1347,7 @@ pynina==0.3.0
|
||||
pynobo==1.6.0
|
||||
|
||||
# homeassistant.components.nuki
|
||||
pynuki==1.6.1
|
||||
pynuki==1.6.2
|
||||
|
||||
# homeassistant.components.nut
|
||||
pynut2==2.1.2
|
||||
@@ -1382,7 +1382,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.7.9
|
||||
pyoverkiz==1.8.0
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@@ -1532,7 +1532,7 @@ python-nest==4.2.0
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.1.0
|
||||
python-otbr-api==2.2.0
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api==1.1.0
|
||||
@@ -1541,7 +1541,7 @@ python-picnic-api==1.1.0
|
||||
python-qbittorrent==0.4.2
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.17.0
|
||||
python-roborock==0.23.4
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.9.1
|
||||
pyunifiprotect==4.10.2
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@@ -1947,7 +1947,7 @@ wyoming==0.0.1
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.17.0
|
||||
xiaomi-ble==0.17.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.10.0
|
||||
@@ -1986,7 +1986,7 @@ youless-api==1.0.1
|
||||
zamg==0.2.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.63.0
|
||||
zeroconf==0.64.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.1
|
||||
|
||||
@@ -132,9 +132,8 @@ authlib<1.0
|
||||
# Version 2.0 added typing, prevent accidental fallbacks
|
||||
backoff>=2.0
|
||||
|
||||
# Breaking change in version
|
||||
# https://github.com/samuelcolvin/pydantic/issues/4092
|
||||
pydantic!=1.9.1
|
||||
# Require to avoid issues with decorators (#93904). v2 has breaking changes.
|
||||
pydantic>=1.10.8,<2.0
|
||||
|
||||
# Breaks asyncio
|
||||
# https://github.com/pubnub/python/issues/130
|
||||
|
||||
@@ -39,7 +39,7 @@ async def test_attributes(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get("sensor.environment_sensor_lux")
|
||||
assert state.state == "1.0"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "lux"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "lx"
|
||||
|
||||
state = hass.states.get("sensor.environment_sensor_temperature")
|
||||
# Abodepy device JSON reports 19.5, but Home Assistant shows 19.4
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"""The climate tests for the Airzone platform."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairzone.common import OperationMode
|
||||
from aioairzone.const import (
|
||||
API_COOL_SET_POINT,
|
||||
API_DATA,
|
||||
API_HEAT_SET_POINT,
|
||||
API_MAX_TEMP,
|
||||
API_MIN_TEMP,
|
||||
API_MODE,
|
||||
API_ON,
|
||||
API_SET_POINT,
|
||||
API_SPEED,
|
||||
@@ -336,7 +334,6 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
|
||||
{
|
||||
API_SYSTEM_ID: 1,
|
||||
API_ZONE_ID: 1,
|
||||
API_MODE: OperationMode.COOLING.value,
|
||||
API_ON: 1,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -712,6 +712,30 @@ async def test_zeroconf_flow_already_configured_host_not_changed_no_reload_entry
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_zeroconf_flow_abort_if_mac_is_missing(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test when mac is missing in the zeroconf discovery we abort."""
|
||||
host = "1.2.3.4"
|
||||
name = "My Android TV"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host=host,
|
||||
addresses=[host],
|
||||
port=6466,
|
||||
hostname=host,
|
||||
type="mock_type",
|
||||
name=name + "._androidtvremote2._tcp.local.",
|
||||
properties={},
|
||||
),
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_reauth_flow_success(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"""The tests for the Command line Binary sensor platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.command_line.binary_sensor import CommandBinarySensor
|
||||
from homeassistant.components.command_line.const import DOMAIN
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_setup_platform_yaml(hass: HomeAssistant) -> None:
|
||||
@@ -189,3 +196,59 @@ async def test_return_code(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "return code 33" in caplog.text
|
||||
|
||||
|
||||
async def test_updating_to_often(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test handling updating when command already running."""
|
||||
called = []
|
||||
|
||||
class MockCommandBinarySensor(CommandBinarySensor):
|
||||
"""Mock entity that updates slow."""
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update slow."""
|
||||
called.append(1)
|
||||
# Add waiting time
|
||||
await asyncio.sleep(1)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.command_line.binary_sensor.CommandBinarySensor",
|
||||
side_effect=MockCommandBinarySensor,
|
||||
):
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"binary_sensor": {
|
||||
"name": "Test",
|
||||
"command": "echo 1",
|
||||
"payload_on": "1",
|
||||
"payload_off": "0",
|
||||
"scan_interval": 0.1,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 1
|
||||
assert (
|
||||
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 2
|
||||
assert (
|
||||
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""The tests the cover command line platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
@@ -9,6 +11,7 @@ import pytest
|
||||
|
||||
from homeassistant import config as hass_config, setup
|
||||
from homeassistant.components.command_line import DOMAIN
|
||||
from homeassistant.components.command_line.cover import CommandCover
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -320,3 +323,58 @@ async def test_unique_id(
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"cover", "command_line", "not-so-unique-anymore"
|
||||
)
|
||||
|
||||
|
||||
async def test_updating_to_often(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test handling updating when command already running."""
|
||||
called = []
|
||||
|
||||
class MockCommandCover(CommandCover):
|
||||
"""Mock entity that updates slow."""
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update slow."""
|
||||
called.append(1)
|
||||
# Add waiting time
|
||||
await asyncio.sleep(1)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.command_line.cover.CommandCover",
|
||||
side_effect=MockCommandCover,
|
||||
):
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"cover": {
|
||||
"command_state": "echo 1",
|
||||
"value_template": "{{ value }}",
|
||||
"name": "Test",
|
||||
"scan_interval": 0.1,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 0
|
||||
assert (
|
||||
"Updating Command Line Cover Test took longer than the scheduled update interval"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 1
|
||||
assert (
|
||||
"Updating Command Line Cover Test took longer than the scheduled update interval"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for the Command line sensor platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
@@ -9,6 +10,7 @@ import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.command_line import DOMAIN
|
||||
from homeassistant.components.command_line.sensor import CommandSensor
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -530,3 +532,57 @@ async def test_unique_id(
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"sensor", "command_line", "not-so-unique-anymore"
|
||||
)
|
||||
|
||||
|
||||
async def test_updating_to_often(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test handling updating when command already running."""
|
||||
called = []
|
||||
|
||||
class MockCommandSensor(CommandSensor):
|
||||
"""Mock entity that updates slow."""
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update slow."""
|
||||
called.append(1)
|
||||
# Add waiting time
|
||||
await asyncio.sleep(1)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.command_line.sensor.CommandSensor",
|
||||
side_effect=MockCommandSensor,
|
||||
):
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"sensor": {
|
||||
"name": "Test",
|
||||
"command": "echo 1",
|
||||
"scan_interval": 0.1,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 1
|
||||
assert (
|
||||
"Updating Command Line Sensor Test took longer than the scheduled update interval"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 2
|
||||
assert (
|
||||
"Updating Command Line Sensor Test took longer than the scheduled update interval"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""The tests for the Command line switch platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
@@ -11,6 +13,7 @@ import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.command_line import DOMAIN
|
||||
from homeassistant.components.command_line.switch import CommandSwitch
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -637,3 +640,59 @@ async def test_templating(hass: HomeAssistant) -> None:
|
||||
assert entity_state.attributes.get("icon") == "mdi:on"
|
||||
assert entity_state2.state == STATE_ON
|
||||
assert entity_state2.attributes.get("icon") == "mdi:on"
|
||||
|
||||
|
||||
async def test_updating_to_often(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test handling updating when command already running."""
|
||||
called = []
|
||||
|
||||
class MockCommandSwitch(CommandSwitch):
|
||||
"""Mock entity that updates slow."""
|
||||
|
||||
async def _async_update(self) -> None:
|
||||
"""Update slow."""
|
||||
called.append(1)
|
||||
# Add waiting time
|
||||
await asyncio.sleep(1)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.command_line.switch.CommandSwitch",
|
||||
side_effect=MockCommandSwitch,
|
||||
):
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"switch": {
|
||||
"command_state": "echo 1",
|
||||
"command_on": "echo 2",
|
||||
"command_off": "echo 3",
|
||||
"name": "Test",
|
||||
"scan_interval": 0.1,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 0
|
||||
assert (
|
||||
"Updating Command Line Switch Test took longer than the scheduled update interval"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(called) == 1
|
||||
assert (
|
||||
"Updating Command Line Switch Test took longer than the scheduled update interval"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
@@ -6,7 +6,9 @@ import time
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from gspread.exceptions import APIError
|
||||
import pytest
|
||||
from requests.models import Response
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
@@ -15,7 +17,7 @@ from homeassistant.components.application_credentials import (
|
||||
from homeassistant.components.google_sheets import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -212,6 +214,37 @@ async def test_append_sheet(
|
||||
assert len(mock_client.mock_calls) == 8
|
||||
|
||||
|
||||
async def test_append_sheet_api_error(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test append to sheet service call API error."""
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.LOADED
|
||||
|
||||
response = Response()
|
||||
response.status_code = 503
|
||||
|
||||
with pytest.raises(HomeAssistantError), patch(
|
||||
"homeassistant.components.google_sheets.Client.request",
|
||||
side_effect=APIError(response),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"append_sheet",
|
||||
{
|
||||
"config_entry": config_entry.entry_id,
|
||||
"worksheet": "Sheet1",
|
||||
"data": {"foo": "bar"},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_append_sheet_invalid_config_entry(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
|
||||
@@ -24,7 +24,12 @@ TEST_MESSAGE_HEADERS2 = (
|
||||
b"Subject: Test subject\r\n"
|
||||
)
|
||||
|
||||
TEST_MESSAGE_HEADERS3 = b""
|
||||
|
||||
TEST_MESSAGE = TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2
|
||||
TEST_MESSAGE_NO_SUBJECT_TO_FROM = (
|
||||
TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS3
|
||||
)
|
||||
TEST_MESSAGE_ALT = TEST_MESSAGE_HEADERS1 + DATE_HEADER2 + TEST_MESSAGE_HEADERS2
|
||||
TEST_INVALID_DATE1 = (
|
||||
TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID1 + TEST_MESSAGE_HEADERS2
|
||||
@@ -204,4 +209,19 @@ TEST_FETCH_RESPONSE_MULTIPART = (
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM = (
|
||||
"OK",
|
||||
[
|
||||
b"1 FETCH (BODY[] {"
|
||||
+ str(len(TEST_MESSAGE_NO_SUBJECT_TO_FROM + TEST_CONTENT_TEXT_PLAIN)).encode(
|
||||
"utf-8"
|
||||
)
|
||||
+ b"}",
|
||||
bytearray(TEST_MESSAGE_NO_SUBJECT_TO_FROM + TEST_CONTENT_TEXT_PLAIN),
|
||||
b")",
|
||||
b"Fetch completed (0.0001 + 0.000 secs).",
|
||||
],
|
||||
)
|
||||
|
||||
RESPONSE_BAD = ("BAD", [])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test the imap entry initialization."""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import (
|
||||
TEST_FETCH_RESPONSE_INVALID_DATE2,
|
||||
TEST_FETCH_RESPONSE_INVALID_DATE3,
|
||||
TEST_FETCH_RESPONSE_MULTIPART,
|
||||
TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM,
|
||||
TEST_FETCH_RESPONSE_TEXT_BARE,
|
||||
TEST_FETCH_RESPONSE_TEXT_OTHER,
|
||||
TEST_FETCH_RESPONSE_TEXT_PLAIN,
|
||||
@@ -153,6 +154,44 @@ async def test_receiving_message_successfully(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE])
|
||||
@pytest.mark.parametrize("imap_fetch", [TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM])
|
||||
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
||||
async def test_receiving_message_no_subject_to_from(
|
||||
hass: HomeAssistant, mock_imap_protocol: MagicMock
|
||||
) -> None:
|
||||
"""Test receiving a message successfully without subject, to and from in body."""
|
||||
event_called = async_capture_events(hass, "imap_content")
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
# Make sure we have had one update (when polling)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=5))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.imap_email_email_com")
|
||||
# we should have received one message
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
|
||||
# we should have received one event
|
||||
assert len(event_called) == 1
|
||||
data: dict[str, Any] = event_called[0].data
|
||||
assert data["server"] == "imap.server.com"
|
||||
assert data["username"] == "email@email.com"
|
||||
assert data["search"] == "UnSeen UnDeleted"
|
||||
assert data["folder"] == "INBOX"
|
||||
assert data["sender"] == ""
|
||||
assert data["subject"] == ""
|
||||
assert data["date"] == datetime(
|
||||
2023, 3, 24, 13, 52, tzinfo=timezone(timedelta(seconds=3600))
|
||||
)
|
||||
assert data["text"] == "Test body\r\n\r\n"
|
||||
assert data["headers"]["Return-Path"] == ("<john.doe@example.com>",)
|
||||
assert data["headers"]["Delivered-To"] == ("notify@example.com",)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
||||
@pytest.mark.parametrize(
|
||||
("imap_login_state", "success"), [(AUTH, True), (NONAUTH, False)]
|
||||
|
||||
@@ -102,12 +102,13 @@ async def test_select_option(hass: HomeAssistant) -> None:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "another option"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "non existing option"},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "non existing option"},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "another option"
|
||||
|
||||
@@ -305,12 +306,13 @@ async def test_set_options_service(hass: HomeAssistant) -> None:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "test1"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "first option"},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "first option"},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "test1"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.state import async_reproduce_state
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -60,7 +61,8 @@ async def test_reproducing_states(
|
||||
assert hass.states.get(ENTITY).state == VALID_OPTION3
|
||||
|
||||
# Test setting state to invalid state
|
||||
await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)])
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)])
|
||||
|
||||
# The entity state should be unchanged
|
||||
assert hass.states.get(ENTITY).state == VALID_OPTION3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The tests for lastfm."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pylast import Track, WSError
|
||||
from pylast import PyLastError, Track
|
||||
|
||||
from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
@@ -65,7 +65,7 @@ class MockUser:
|
||||
def get_friends(self):
|
||||
"""Get mock friends."""
|
||||
if self._has_friends is False:
|
||||
raise WSError("network", "status", "Page not found")
|
||||
raise PyLastError("network", "status", "Page not found")
|
||||
return [MockUser(None, None, True, USERNAME_2)]
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_async_browse_media(hass: HomeAssistant) -> None:
|
||||
media = await media_source.async_browse_media(hass, const.URI_SCHEME)
|
||||
assert isinstance(media, media_source.models.BrowseMediaSource)
|
||||
assert len(media.children) == 1
|
||||
assert media.children[0].title == "Local Media"
|
||||
assert media.children[0].title == "My media"
|
||||
|
||||
|
||||
async def test_async_resolve_media(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -84,6 +84,8 @@ async def test_create_network(
|
||||
with patch(
|
||||
"python_otbr_api.OTBR.create_active_dataset"
|
||||
) as create_dataset_mock, patch(
|
||||
"python_otbr_api.OTBR.delete_active_dataset"
|
||||
) as delete_dataset_mock, patch(
|
||||
"python_otbr_api.OTBR.set_enabled"
|
||||
) as set_enabled_mock, patch(
|
||||
"python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16
|
||||
@@ -99,6 +101,7 @@ async def test_create_network(
|
||||
create_dataset_mock.assert_called_once_with(
|
||||
python_otbr_api.models.ActiveDataSet(channel=15, network_name="home-assistant")
|
||||
)
|
||||
delete_dataset_mock.assert_called_once_with()
|
||||
assert len(set_enabled_mock.mock_calls) == 2
|
||||
assert set_enabled_mock.mock_calls[0][1][0] is False
|
||||
assert set_enabled_mock.mock_calls[1][1][0] is True
|
||||
@@ -151,7 +154,7 @@ async def test_create_network_fails_2(
|
||||
), patch(
|
||||
"python_otbr_api.OTBR.create_active_dataset",
|
||||
side_effect=python_otbr_api.OTBRError,
|
||||
):
|
||||
), patch("python_otbr_api.OTBR.delete_active_dataset"):
|
||||
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
|
||||
msg = await websocket_client.receive_json()
|
||||
|
||||
@@ -171,6 +174,8 @@ async def test_create_network_fails_3(
|
||||
side_effect=[None, python_otbr_api.OTBRError],
|
||||
), patch(
|
||||
"python_otbr_api.OTBR.create_active_dataset",
|
||||
), patch(
|
||||
"python_otbr_api.OTBR.delete_active_dataset"
|
||||
):
|
||||
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
|
||||
msg = await websocket_client.receive_json()
|
||||
@@ -191,6 +196,8 @@ async def test_create_network_fails_4(
|
||||
), patch(
|
||||
"python_otbr_api.OTBR.get_active_dataset_tlvs",
|
||||
side_effect=python_otbr_api.OTBRError,
|
||||
), patch(
|
||||
"python_otbr_api.OTBR.delete_active_dataset"
|
||||
):
|
||||
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
|
||||
msg = await websocket_client.receive_json()
|
||||
@@ -208,7 +215,9 @@ async def test_create_network_fails_5(
|
||||
"""Test create network."""
|
||||
with patch("python_otbr_api.OTBR.set_enabled"), patch(
|
||||
"python_otbr_api.OTBR.create_active_dataset"
|
||||
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None):
|
||||
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch(
|
||||
"python_otbr_api.OTBR.delete_active_dataset"
|
||||
):
|
||||
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
|
||||
msg = await websocket_client.receive_json()
|
||||
|
||||
@@ -216,6 +225,26 @@ async def test_create_network_fails_5(
|
||||
assert msg["error"]["code"] == "get_active_dataset_tlvs_empty"
|
||||
|
||||
|
||||
async def test_create_network_fails_6(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
otbr_config_entry,
|
||||
websocket_client,
|
||||
) -> None:
|
||||
"""Test create network."""
|
||||
with patch("python_otbr_api.OTBR.set_enabled"), patch(
|
||||
"python_otbr_api.OTBR.create_active_dataset"
|
||||
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch(
|
||||
"python_otbr_api.OTBR.delete_active_dataset",
|
||||
side_effect=python_otbr_api.OTBRError,
|
||||
):
|
||||
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
|
||||
msg = await websocket_client.receive_json()
|
||||
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "delete_active_dataset_failed"
|
||||
|
||||
|
||||
async def test_set_network(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
|
||||
@@ -25,7 +25,6 @@ async def test_create(hass: HomeAssistant) -> None:
|
||||
assert len(notifications) == 1
|
||||
|
||||
notification = notifications[list(notifications)[0]]
|
||||
assert notification["status"] == pn.STATUS_UNREAD
|
||||
assert notification["message"] == "Hello World 2"
|
||||
assert notification["title"] == "2 beers"
|
||||
assert notification["created_at"] is not None
|
||||
@@ -66,39 +65,6 @@ async def test_dismiss_notification(hass: HomeAssistant) -> None:
|
||||
assert len(notifications) == 0
|
||||
|
||||
|
||||
async def test_mark_read(hass: HomeAssistant) -> None:
|
||||
"""Ensure notification is marked as Read."""
|
||||
notifications = pn._async_get_or_create_notifications(hass)
|
||||
assert len(notifications) == 0
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN,
|
||||
"create",
|
||||
{"notification_id": "Beer 2", "message": "test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(notifications) == 1
|
||||
notification = notifications[list(notifications)[0]]
|
||||
assert notification["status"] == pn.STATUS_UNREAD
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True
|
||||
)
|
||||
|
||||
assert len(notifications) == 1
|
||||
notification = notifications[list(notifications)[0]]
|
||||
assert notification["status"] == pn.STATUS_READ
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN,
|
||||
"dismiss",
|
||||
{"notification_id": "Beer 2"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(notifications) == 0
|
||||
|
||||
|
||||
async def test_ws_get_notifications(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
@@ -128,19 +94,8 @@ async def test_ws_get_notifications(
|
||||
assert notification["notification_id"] == "Beer 2"
|
||||
assert notification["message"] == "test"
|
||||
assert notification["title"] is None
|
||||
assert notification["status"] == pn.STATUS_UNREAD
|
||||
assert notification["created_at"] is not None
|
||||
|
||||
# Mark Read
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}
|
||||
)
|
||||
await client.send_json({"id": 7, "type": "persistent_notification/get"})
|
||||
msg = await client.receive_json()
|
||||
notifications = msg["result"]
|
||||
assert len(notifications) == 1
|
||||
assert notifications[0]["status"] == pn.STATUS_READ
|
||||
|
||||
# Dismiss
|
||||
pn.async_dismiss(hass, "Beer 2")
|
||||
await client.send_json({"id": 8, "type": "persistent_notification/get"})
|
||||
@@ -186,24 +141,8 @@ async def test_ws_get_subscribe(
|
||||
assert notification["notification_id"] == "Beer 2"
|
||||
assert notification["message"] == "test"
|
||||
assert notification["title"] is None
|
||||
assert notification["status"] == pn.STATUS_UNREAD
|
||||
assert notification["created_at"] is not None
|
||||
|
||||
# Mark Read
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["id"] == 5
|
||||
assert msg["type"] == "event"
|
||||
assert msg["event"]
|
||||
event = msg["event"]
|
||||
assert event["type"] == "updated"
|
||||
notifications = event["notifications"]
|
||||
assert len(notifications) == 1
|
||||
notification = notifications[list(notifications)[0]]
|
||||
assert notification["status"] == pn.STATUS_READ
|
||||
|
||||
# Dismiss
|
||||
pn.async_dismiss(hass, "Beer 2")
|
||||
msg = await client.receive_json()
|
||||
@@ -212,3 +151,27 @@ async def test_ws_get_subscribe(
|
||||
assert msg["event"]
|
||||
event = msg["event"]
|
||||
assert event["type"] == "removed"
|
||||
|
||||
|
||||
async def test_manual_notification_id_round_trip(hass: HomeAssistant) -> None:
|
||||
"""Test that a manual notification id can be round tripped."""
|
||||
notifications = pn._async_get_or_create_notifications(hass)
|
||||
assert len(notifications) == 0
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN,
|
||||
"create",
|
||||
{"notification_id": "synology_diskstation_hub_notification", "message": "test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(notifications) == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN,
|
||||
"dismiss",
|
||||
{"notification_id": "synology_diskstation_hub_notification"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(notifications) == 0
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .mock_data import BASE_URL, HOME_DATA, PROP, USER_DATA, USER_EMAIL
|
||||
from .mock_data import BASE_URL, HOME_DATA, NETWORK_INFO, PROP, USER_DATA, USER_EMAIL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -54,7 +54,8 @@ async def setup_entry(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
return_value=HOME_DATA,
|
||||
), patch(
|
||||
"homeassistant.components.roborock.RoborockMqttClient.get_networking"
|
||||
"homeassistant.components.roborock.RoborockMqttClient.get_networking",
|
||||
return_value=NETWORK_INFO,
|
||||
), patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop",
|
||||
return_value=PROP,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Mock data for Roborock tests."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from roborock.containers import (
|
||||
CleanRecord,
|
||||
CleanSummary,
|
||||
@@ -320,6 +322,8 @@ DND_TIMER = DnDTimer.from_dict(
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
DND_TIMER.start_time = datetime.datetime(year=2023, month=6, day=1, hour=22)
|
||||
DND_TIMER.end_time = datetime.datetime(year=2023, month=6, day=2, hour=7)
|
||||
|
||||
STATUS = S7Status.from_dict(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'config_entry': dict({
|
||||
'base_url': 'https://usiot.roborock.com',
|
||||
'user_data': dict({
|
||||
'avatarurl': 'https://files.roborock.com/iottest/default_avatar.png',
|
||||
'country': 'US',
|
||||
'countrycode': '1',
|
||||
'nickname': 'user_nickname',
|
||||
'region': 'us',
|
||||
'rriot': dict({
|
||||
'h': 'abc123',
|
||||
'k': 'abc123',
|
||||
'r': dict({
|
||||
'a': 'https://api-us.roborock.com',
|
||||
'l': 'https://wood-us.roborock.com',
|
||||
'm': 'ssl://mqtt-us-2.roborock.com:8883',
|
||||
'r': 'US',
|
||||
}),
|
||||
's': 'abc123',
|
||||
'u': 'abc123',
|
||||
}),
|
||||
'rruid': '**REDACTED**',
|
||||
'token': '**REDACTED**',
|
||||
'tokentype': '',
|
||||
'tuyaDeviceState': 2,
|
||||
'uid': '**REDACTED**',
|
||||
}),
|
||||
'username': '**REDACTED**',
|
||||
}),
|
||||
'coordinators': dict({
|
||||
'**REDACTED-0**': dict({
|
||||
'api': dict({
|
||||
}),
|
||||
'roborock_device_info': dict({
|
||||
'device': dict({
|
||||
'activeTime': 1672364449,
|
||||
'deviceStatus': dict({
|
||||
'120': 0,
|
||||
'121': 8,
|
||||
'122': 100,
|
||||
'123': 102,
|
||||
'124': 203,
|
||||
'125': 94,
|
||||
'126': 90,
|
||||
'127': 87,
|
||||
'128': 0,
|
||||
'133': 1,
|
||||
}),
|
||||
'duid': '**REDACTED**',
|
||||
'extra': '{"RRPhotoPrivacyVersion": "1"}',
|
||||
'featureSet': '2234201184108543',
|
||||
'fv': '02.56.02',
|
||||
'iconUrl': '',
|
||||
'localKey': '**REDACTED**',
|
||||
'name': 'Roborock S7 MaxV',
|
||||
'newFeatureSet': '0000000000002041',
|
||||
'online': True,
|
||||
'productId': 'abc123',
|
||||
'pv': '1.0',
|
||||
'roomId': 2362003,
|
||||
'share': False,
|
||||
'silentOtaSwitch': True,
|
||||
'sn': 'abc123',
|
||||
'timeZoneId': 'America/Los_Angeles',
|
||||
'tuyaMigrated': False,
|
||||
}),
|
||||
'network_info': dict({
|
||||
'bssid': '**REDACTED**',
|
||||
'ip': '123.232.12.1',
|
||||
'mac': '**REDACTED**',
|
||||
'rssi': 90,
|
||||
'ssid': 'wifi',
|
||||
}),
|
||||
'product': dict({
|
||||
'capability': 0,
|
||||
'category': 'robot.vacuum.cleaner',
|
||||
'code': 'a27',
|
||||
'id': 'abc123',
|
||||
'model': 'roborock.vacuum.a27',
|
||||
'name': 'Roborock S7 MaxV',
|
||||
'schema': list([
|
||||
dict({
|
||||
'code': 'rpc_request',
|
||||
'id': '101',
|
||||
'mode': 'rw',
|
||||
'name': 'rpc_request',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'rpc_response',
|
||||
'id': '102',
|
||||
'mode': 'rw',
|
||||
'name': 'rpc_response',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'error_code',
|
||||
'id': '120',
|
||||
'mode': 'ro',
|
||||
'name': '错误代码',
|
||||
'type': 'ENUM',
|
||||
}),
|
||||
dict({
|
||||
'code': 'state',
|
||||
'id': '121',
|
||||
'mode': 'ro',
|
||||
'name': '设备状态',
|
||||
'type': 'ENUM',
|
||||
}),
|
||||
dict({
|
||||
'code': 'battery',
|
||||
'id': '122',
|
||||
'mode': 'ro',
|
||||
'name': '设备电量',
|
||||
'type': 'ENUM',
|
||||
}),
|
||||
dict({
|
||||
'code': 'fan_power',
|
||||
'id': '123',
|
||||
'mode': 'rw',
|
||||
'name': '清扫模式',
|
||||
'type': 'ENUM',
|
||||
}),
|
||||
dict({
|
||||
'code': 'water_box_mode',
|
||||
'id': '124',
|
||||
'mode': 'rw',
|
||||
'name': '拖地模式',
|
||||
'type': 'ENUM',
|
||||
}),
|
||||
dict({
|
||||
'code': 'main_brush_life',
|
||||
'id': '125',
|
||||
'mode': 'rw',
|
||||
'name': '主刷寿命',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'side_brush_life',
|
||||
'id': '126',
|
||||
'mode': 'rw',
|
||||
'name': '边刷寿命',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'filter_life',
|
||||
'id': '127',
|
||||
'mode': 'rw',
|
||||
'name': '滤网寿命',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'additional_props',
|
||||
'id': '128',
|
||||
'mode': 'ro',
|
||||
'name': '额外状态',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'task_complete',
|
||||
'id': '130',
|
||||
'mode': 'ro',
|
||||
'name': '完成事件',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'task_cancel_low_power',
|
||||
'id': '131',
|
||||
'mode': 'ro',
|
||||
'name': '电量不足任务取消',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'task_cancel_in_motion',
|
||||
'id': '132',
|
||||
'mode': 'ro',
|
||||
'name': '运动中任务取消',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'charge_status',
|
||||
'id': '133',
|
||||
'mode': 'ro',
|
||||
'name': '充电状态',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'drying_status',
|
||||
'id': '134',
|
||||
'mode': 'ro',
|
||||
'name': '烘干状态',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
'props': dict({
|
||||
'cleanSummary': dict({
|
||||
'cleanArea': 1159182500,
|
||||
'cleanCount': 31,
|
||||
'cleanTime': 74382,
|
||||
'dustCollectionCount': 25,
|
||||
'records': list([
|
||||
1672543330,
|
||||
1672458041,
|
||||
]),
|
||||
'squareMeterCleanArea': 1159.2,
|
||||
}),
|
||||
'consumable': dict({
|
||||
'cleaningBrushWorkTimes': 65,
|
||||
'dustCollectionWorkTimes': 25,
|
||||
'filterElementWorkTime': 0,
|
||||
'filterTimeLeft': 465618,
|
||||
'filterWorkTime': 74382,
|
||||
'mainBrushTimeLeft': 1005618,
|
||||
'mainBrushWorkTime': 74382,
|
||||
'sensorDirtyTime': 74382,
|
||||
'sensorTimeLeft': 33618,
|
||||
'sideBrushTimeLeft': 645618,
|
||||
'sideBrushWorkTime': 74382,
|
||||
'strainerWorkTimes': 65,
|
||||
}),
|
||||
'dndTimer': dict({
|
||||
'enabled': 1,
|
||||
'endHour': 7,
|
||||
'endMinute': 0,
|
||||
'endTime': '2023-06-02T07:00:00',
|
||||
'startHour': 22,
|
||||
'startMinute': 0,
|
||||
'startTime': '2023-06-01T22:00:00',
|
||||
}),
|
||||
'lastCleanRecord': dict({
|
||||
'area': 20965000,
|
||||
'avoidCount': 19,
|
||||
'begin': 1672543330,
|
||||
'cleanType': 3,
|
||||
'complete': 1,
|
||||
'duration': 1176,
|
||||
'dustCollectionStatus': 1,
|
||||
'end': 1672544638,
|
||||
'error': 0,
|
||||
'finishReason': 56,
|
||||
'mapFlag': 0,
|
||||
'squareMeterArea': 21.0,
|
||||
'startType': 2,
|
||||
'washCount': 2,
|
||||
}),
|
||||
'status': dict({
|
||||
'adbumperStatus': list([
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'autoDustCollection': 1,
|
||||
'avoidCount': 19,
|
||||
'backType': -1,
|
||||
'battery': 100,
|
||||
'cameraStatus': 3457,
|
||||
'chargeStatus': 1,
|
||||
'cleanArea': 20965000,
|
||||
'cleanTime': 1176,
|
||||
'collisionAvoidStatus': 1,
|
||||
'debugMode': 0,
|
||||
'dndEnabled': 0,
|
||||
'dockErrorStatus': 0,
|
||||
'dockType': 3,
|
||||
'dustCollectionStatus': 0,
|
||||
'errorCode': 0,
|
||||
'fanPower': 102,
|
||||
'homeSecEnablePassword': 0,
|
||||
'homeSecStatus': 0,
|
||||
'inCleaning': 0,
|
||||
'inFreshState': 1,
|
||||
'inReturning': 0,
|
||||
'isExploring': 0,
|
||||
'isLocating': 0,
|
||||
'labStatus': 1,
|
||||
'lockStatus': 0,
|
||||
'mapPresent': 1,
|
||||
'mapStatus': 3,
|
||||
'mopForbiddenEnable': 1,
|
||||
'mopMode': 300,
|
||||
'msgSeq': 458,
|
||||
'msgVer': 2,
|
||||
'squareMeterCleanArea': 21.0,
|
||||
'state': 8,
|
||||
'switchMapMode': 0,
|
||||
'unsaveMapFlag': 0,
|
||||
'unsaveMapReason': 0,
|
||||
'washPhase': 0,
|
||||
'washReady': 0,
|
||||
'waterBoxCarriageStatus': 1,
|
||||
'waterBoxMode': 203,
|
||||
'waterBoxStatus': 1,
|
||||
'waterShortageStatus': 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Tests for the diagnostics data provided by the Roborock integration."""
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics for config entry."""
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result == snapshot
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user