mirror of
https://github.com/home-assistant/core.git
synced 2026-06-27 17:15:23 +02:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03fb4b099c | |||
| 46a6048f60 | |||
| c598d2c10e | |||
| 67bcd7550c | |||
| e44e822cec | |||
| daff150276 | |||
| 1f33859297 | |||
| 512fe8c022 | |||
| 6f038bb5b2 | |||
| d0b5162507 | |||
| 9e9978b6cb | |||
| 76feb821f4 | |||
| cd41529a89 | |||
| 8a1434332d | |||
| 36b714b513 | |||
| 46f1e4c957 | |||
| 431dcda092 | |||
| c575ef51b9 | |||
| 87690d2000 | |||
| 7afb26b1c0 | |||
| b6d5af0480 | |||
| f56098df5f | |||
| 427dd028f5 | |||
| cef9461610 | |||
| ace5398012 | |||
| 3791c83b95 | |||
| 4bdfa5c25b | |||
| 5a60771a14 | |||
| 70aba68326 | |||
| f20f86a067 | |||
| ee0c98e450 | |||
| 907a5c3c6c | |||
| a1e1b400f3 | |||
| 1544ae83dd | |||
| 145c490816 | |||
| bb7a756f84 | |||
| 183e6af8c2 | |||
| 2b66d045ff | |||
| 4841329814 | |||
| e710fc8782 | |||
| 99e18dcdd8 | |||
| eeedf28b6f | |||
| 5cca9328d6 | |||
| ebf3de3073 | |||
| 41e79927d0 | |||
| 4022eb93de | |||
| 28171dfe90 | |||
| e9af932fbe | |||
| 3212e0f051 | |||
| 87f0720450 | |||
| 534ff3f3dc | |||
| 1096c8af13 |
Generated
-1
@@ -181,7 +181,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||
/homeassistant/components/atag/ @MatsNL
|
||||
/tests/components/atag/ @MatsNL
|
||||
/homeassistant/components/aten_pe/ @mtdcr
|
||||
/homeassistant/components/atome/ @baqs
|
||||
/homeassistant/components/august/ @bdraco
|
||||
/tests/components/august/ @bdraco
|
||||
|
||||
@@ -27,7 +27,7 @@ from .const import CONDITIONS_MAP, DOMAIN, FORECAST_MAP
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_TIMEOUT: Final[int] = 120
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=20)
|
||||
|
||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.1.3"]
|
||||
"requirements": ["aioamazondevices==14.1.8"]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ async def async_update_unique_id(
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{old_key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
DOMAIN, platform, unique_id
|
||||
platform, DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
||||
new_unique_id = unique_id.replace(old_key, new_key)
|
||||
@@ -48,7 +48,7 @@ async def async_remove_entity_from_virtual_group(
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
|
||||
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
@@ -70,7 +70,7 @@ async def async_remove_unsupported_notification_sensors(
|
||||
):
|
||||
unique_id = f"{serial_num}-{notification_key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SENSOR_DOMAIN, unique_id=unique_id
|
||||
SENSOR_DOMAIN, DOMAIN, unique_id=unique_id
|
||||
)
|
||||
is_unsupported = not coordinator.data[serial_num].notifications_supported
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.2.2"]
|
||||
"requirements": ["pyanglianwater==3.2.3"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The ATEN PE component."""
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "aten_pe",
|
||||
"name": "ATEN Rack PDU",
|
||||
"codeowners": ["@mtdcr"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/aten_pe",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["atenpdu==0.3.6"]
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
"""The ATEN PE switch component."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from atenpdu import AtenPE, AtenPEError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTH_KEY = "auth_key"
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_PRIV_KEY = "priv_key"
|
||||
DEFAULT_COMMUNITY = "private"
|
||||
DEFAULT_PORT = "161"
|
||||
DEFAULT_USERNAME = "administrator"
|
||||
|
||||
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_AUTH_KEY): cv.string,
|
||||
vol.Optional(CONF_PRIV_KEY): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the ATEN PE switch."""
|
||||
node = config[CONF_HOST]
|
||||
serv = config[CONF_PORT]
|
||||
|
||||
dev = AtenPE(
|
||||
node=node,
|
||||
serv=serv,
|
||||
community=config[CONF_COMMUNITY],
|
||||
username=config[CONF_USERNAME],
|
||||
authkey=config.get(CONF_AUTH_KEY),
|
||||
privkey=config.get(CONF_PRIV_KEY),
|
||||
)
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(dev.initialize)
|
||||
mac = await dev.deviceMAC()
|
||||
outlets = dev.outlets()
|
||||
name = await dev.deviceName()
|
||||
model = await dev.modelName()
|
||||
sw_version = await dev.deviceFWversion()
|
||||
except AtenPEError as exc:
|
||||
_LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc))
|
||||
raise PlatformNotReady from exc
|
||||
|
||||
info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer="ATEN",
|
||||
model=model,
|
||||
name=name,
|
||||
sw_version=sw_version,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
(AtenSwitch(dev, info, mac, outlet.id, outlet.name) for outlet in outlets), True
|
||||
)
|
||||
|
||||
|
||||
class AtenSwitch(SwitchEntity):
|
||||
"""Represents an ATEN PE switch."""
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.OUTLET
|
||||
|
||||
def __init__(
|
||||
self, device: AtenPE, info: DeviceInfo, mac: str, outlet: str, name: str
|
||||
) -> None:
|
||||
"""Initialize an ATEN PE switch."""
|
||||
self._device = device
|
||||
self._outlet = outlet
|
||||
self._attr_device_info = info
|
||||
self._attr_unique_id = f"{mac}-{outlet}"
|
||||
self._attr_name = name or f"Outlet {outlet}"
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._device.setOutletStatus(self._outlet, "on")
|
||||
self._attr_is_on = True
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._device.setOutletStatus(self._outlet, "off")
|
||||
self._attr_is_on = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Process update from entity."""
|
||||
status = await self._device.displayOutletStatus(self._outlet)
|
||||
if status == "on":
|
||||
self._attr_is_on = True
|
||||
elif status == "off":
|
||||
self._attr_is_on = False
|
||||
@@ -731,17 +731,32 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
trace_element = TraceElement(variables, trigger_path)
|
||||
trace_append_element(trace_element)
|
||||
|
||||
if (
|
||||
not skip_condition
|
||||
and self._condition is not None
|
||||
and not self._condition.async_check(variables=variables)
|
||||
):
|
||||
self._logger.debug(
|
||||
"Conditions not met, aborting automation. Condition summary: %s",
|
||||
trace_get(clear=False),
|
||||
)
|
||||
script_execution_set("failed_conditions")
|
||||
return None
|
||||
if not skip_condition and self._condition is not None:
|
||||
try:
|
||||
conditions_pass = self._condition.async_check(variables=variables)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
self._logger.error(
|
||||
"Error while checking conditions of automation %s: %s",
|
||||
self.entity_id,
|
||||
err,
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
return None
|
||||
except Exception as err:
|
||||
self._logger.exception(
|
||||
"Unexpected error while checking conditions of automation %s",
|
||||
self.entity_id,
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
return None
|
||||
|
||||
if not conditions_pass:
|
||||
self._logger.debug(
|
||||
"Conditions not met, aborting automation. Condition summary: %s",
|
||||
trace_get(clear=False),
|
||||
)
|
||||
script_execution_set("failed_conditions")
|
||||
return None
|
||||
|
||||
self.async_set_context(trigger_context)
|
||||
event_data = {
|
||||
@@ -794,7 +809,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
except Exception as err:
|
||||
self._logger.exception("While executing automation %s", self.entity_id)
|
||||
self._logger.exception(
|
||||
"Unexpected error while executing automation %s", self.entity_id
|
||||
)
|
||||
automation_trace.set_error(err)
|
||||
|
||||
return None
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.22",
|
||||
"habluetooth==6.23.1"
|
||||
"habluetooth==6.25.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -805,6 +805,10 @@ class DefaultAgent(ConversationEntity):
|
||||
else:
|
||||
num_unmatched_entities += 1
|
||||
|
||||
# Literal text matched is the dominant signal
|
||||
same_text_matched = (maybe_result is not None) and (
|
||||
result.text_chunks_matched == maybe_result.text_chunks_matched
|
||||
)
|
||||
if (
|
||||
(maybe_result is None) # first result
|
||||
or (
|
||||
@@ -813,22 +817,25 @@ class DefaultAgent(ConversationEntity):
|
||||
)
|
||||
or (
|
||||
# More entities matched
|
||||
num_matched_entities > best_num_matched_entities
|
||||
same_text_matched
|
||||
and (num_matched_entities > best_num_matched_entities)
|
||||
)
|
||||
or (
|
||||
# Fewer unmatched entities
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
same_text_matched
|
||||
and (num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities < best_num_unmatched_entities)
|
||||
)
|
||||
or (
|
||||
# Prefer unmatched ranges
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
same_text_matched
|
||||
and (num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||
)
|
||||
or (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
same_text_matched
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||
and (
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.8.0", "home-assistant-intents==2026.6.1"]
|
||||
"requirements": ["hassil==3.8.0", "home-assistant-intents==2026.6.24"]
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Support for Dovado router."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
# import dovado
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
DEVICE_DEFAULT_NAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "dovado"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Dovado component."""
|
||||
|
||||
hass.data[DOMAIN] = DovadoData(
|
||||
dovado.Dovado(
|
||||
config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD],
|
||||
config[DOMAIN].get(CONF_HOST),
|
||||
config[DOMAIN].get(CONF_PORT),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class DovadoData:
|
||||
"""Maintain a connection to the router."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Set up a new Dovado connection."""
|
||||
self._client = client
|
||||
self.state = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the router."""
|
||||
return self.state.get("product name", DEVICE_DEFAULT_NAME)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
try:
|
||||
self.state = self._client.state or {}
|
||||
if not self.state:
|
||||
return False
|
||||
self.state.update(connected=self.state.get("modem status") == "CONNECTED")
|
||||
except OSError as error:
|
||||
_LOGGER.warning("Could not contact the router: %s", error)
|
||||
return None
|
||||
_LOGGER.debug("Received: %s", self.state)
|
||||
return True
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Dovado client instance."""
|
||||
return self._client
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "dovado",
|
||||
"name": "Dovado",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dovado",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["dovado==0.4.1"]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Support for SMS notifications from the Dovado router."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> DovadoSMSNotificationService:
|
||||
"""Get the Dovado Router SMS notification service."""
|
||||
return DovadoSMSNotificationService(hass.data[DOMAIN].client)
|
||||
|
||||
|
||||
class DovadoSMSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Dovado SMS component."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
@override
|
||||
def send_message(self, message: str, **kwargs: Any) -> None:
|
||||
"""Send SMS to the specified target phone number."""
|
||||
if not (target := kwargs.get(ATTR_TARGET)):
|
||||
_LOGGER.error("One target is required")
|
||||
return
|
||||
|
||||
self._client.send_sms(target, message)
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
"""Support for sensors from the Dovado router."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import re
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_SENSORS, PERCENTAGE, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
SENSOR_UPLOAD = "upload"
|
||||
SENSOR_DOWNLOAD = "download"
|
||||
SENSOR_SIGNAL = "signal"
|
||||
SENSOR_NETWORK = "network"
|
||||
SENSOR_SMS_UNREAD = "sms"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class DovadoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Dovado sensor entity."""
|
||||
|
||||
identifier: str
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[DovadoSensorEntityDescription, ...] = (
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_NETWORK,
|
||||
key="signal strength",
|
||||
name="Network",
|
||||
icon="mdi:access-point-network",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_SIGNAL,
|
||||
key="signal strength",
|
||||
name="Signal Strength",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:signal",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_SMS_UNREAD,
|
||||
key="sms unread",
|
||||
name="SMS unread",
|
||||
icon="mdi:message-text-outline",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_UPLOAD,
|
||||
key="traffic modem tx",
|
||||
name="Sent",
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:cloud-upload",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_DOWNLOAD,
|
||||
key="traffic modem rx",
|
||||
name="Received",
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:cloud-download",
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)])}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dovado sensor platform."""
|
||||
dovado = hass.data[DOMAIN]
|
||||
|
||||
sensors = config[CONF_SENSORS]
|
||||
entities = [
|
||||
DovadoSensor(dovado, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in sensors
|
||||
]
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class DovadoSensor(SensorEntity):
|
||||
"""Representation of a Dovado sensor."""
|
||||
|
||||
entity_description: DovadoSensorEntityDescription
|
||||
|
||||
def __init__(self, data, description: DovadoSensorEntityDescription) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self._data = data
|
||||
|
||||
self._attr_name = f"{data.name} {description.name}"
|
||||
self._attr_native_value = self._compute_state()
|
||||
|
||||
def _compute_state(self):
|
||||
"""Compute the state of the sensor."""
|
||||
state = self._data.state.get(self.entity_description.key)
|
||||
sensor_identifier = self.entity_description.identifier
|
||||
if sensor_identifier == SENSOR_NETWORK:
|
||||
match = re.search(r"\((.+)\)", state)
|
||||
return match.group(1) if match else None
|
||||
if sensor_identifier == SENSOR_SIGNAL:
|
||||
try:
|
||||
return int(state.split()[0])
|
||||
except ValueError:
|
||||
return None
|
||||
if sensor_identifier == SENSOR_SMS_UNREAD:
|
||||
return int(state)
|
||||
if sensor_identifier in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]:
|
||||
return round(float(state) / 1e6, 1)
|
||||
return state
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update sensor values."""
|
||||
self._data.update()
|
||||
self._attr_native_value = self._compute_state()
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]}
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .auth import DropboxConfigEntryAuth
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
|
||||
|
||||
@@ -31,6 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> b
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
token = entry.data["token"]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_scopes",
|
||||
)
|
||||
if "refresh_token" not in token:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_refresh_token",
|
||||
)
|
||||
|
||||
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
|
||||
|
||||
auth = DropboxConfigEntryAuth(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Application credentials platform for the Dropbox integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -9,14 +7,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||
) -> AbstractOAuth2Implementation:
|
||||
"""Return custom auth implementation."""
|
||||
return DropboxOAuth2Implementation(
|
||||
"""Return auth implementation."""
|
||||
return LocalOAuth2ImplementationWithPkce(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
@@ -24,21 +22,3 @@ async def async_get_auth_implementation(
|
||||
OAUTH2_TOKEN,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Custom Dropbox OAuth2 implementation.
|
||||
|
||||
Adds the necessary authorize url parameters.
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data: dict = {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .auth import DropboxConfigFlowAuth
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
|
||||
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
@@ -26,6 +26,15 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
|
||||
@override
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
@@ -51,6 +60,9 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
token = entry_data[CONF_TOKEN]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
return await self.async_step_reauth_permissions()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
@@ -60,3 +72,11 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reauth_permissions(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that additional permissions are required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_permissions")
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -12,6 +12,7 @@ OAUTH2_SCOPES = [
|
||||
"account_info.read",
|
||||
"files.content.read",
|
||||
"files.content.write",
|
||||
"files.metadata.read",
|
||||
]
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
|
||||
@@ -24,10 +24,20 @@
|
||||
"reauth_confirm": {
|
||||
"description": "The Dropbox integration needs to re-authenticate your account.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"reauth_permissions": {
|
||||
"description": "The Dropbox integration requires additional permissions to function correctly.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"missing_refresh_token": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_confirm::description%]"
|
||||
},
|
||||
"missing_scopes": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_permissions::description%]"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SUPPORTED_SELECT_NODE_TYPES = {
|
||||
NodeType.BOX,
|
||||
NodeType.VLV,
|
||||
NodeType.VLVRH,
|
||||
NodeType.VLVVOC,
|
||||
NodeType.VLVCO2,
|
||||
NodeType.VLVCO2RH,
|
||||
NodeType.EAV,
|
||||
NodeType.EAVRH,
|
||||
NodeType.EAVVOC,
|
||||
NodeType.EAVCO2,
|
||||
}
|
||||
|
||||
|
||||
def _get_ventilation_options(action: ActionItem) -> tuple[str, ...] | None:
|
||||
"""Return ventilation options advertised by a node action."""
|
||||
@@ -71,7 +84,9 @@ async def async_setup_entry(
|
||||
if node.node_id in known_nodes:
|
||||
continue
|
||||
|
||||
if node.general.node_type is not NodeType.BOX:
|
||||
# Duco advertises SetVentilationState broadly, so keep the select
|
||||
# limited to the box and known valve node families.
|
||||
if node.general.node_type not in SUPPORTED_SELECT_NODE_TYPES:
|
||||
continue
|
||||
|
||||
options = options_by_node.get(node.node_id)
|
||||
|
||||
@@ -65,6 +65,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/meters/readings",
|
||||
"/ivp/pdm/device_data",
|
||||
"/home",
|
||||
"/inventory.json?deleted=1",
|
||||
"/admin/lib/acb_config",
|
||||
"/ivp/sc/sched",
|
||||
"/admin/lib/network_display",
|
||||
"/admin/lib/wireless_display",
|
||||
"/ivp/ensemble/relay",
|
||||
"/ivp/livedata/status",
|
||||
"/ivp/pdm/energy",
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
@@ -134,16 +142,15 @@ async def async_get_config_entry_diagnostics(
|
||||
"encharge_power": envoy_data.encharge_power,
|
||||
"encharge_aggregate": envoy_data.encharge_aggregate,
|
||||
"enpower": envoy_data.enpower,
|
||||
"acb_power": envoy_data.acb_power,
|
||||
"acb_inventory": envoy_data.acb_inventory,
|
||||
"battery_aggregate": envoy_data.battery_aggregate,
|
||||
"collar": envoy_data.collar,
|
||||
"c6cc": envoy_data.c6cc,
|
||||
"system_consumption": envoy_data.system_consumption,
|
||||
"system_production": envoy_data.system_production,
|
||||
"system_consumption_phases": envoy_data.system_consumption_phases,
|
||||
"system_production_phases": envoy_data.system_production_phases,
|
||||
"ctmeter_production": envoy_data.ctmeter_production,
|
||||
"ctmeter_consumption": envoy_data.ctmeter_consumption,
|
||||
"ctmeter_storage": envoy_data.ctmeter_storage,
|
||||
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
||||
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
|
||||
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
|
||||
"ctmeters": envoy_data.ctmeters,
|
||||
"ctmeters_phases": envoy_data.ctmeters_phases,
|
||||
"dry_contact_status": envoy_data.dry_contact_status,
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260624.0"]
|
||||
"requirements": ["home-assistant-frontend==20260624.1"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The greenwave component."""
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Support for Greenwave Reality (TCP Connected) lights."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, override
|
||||
|
||||
import greenwavereality as greenwave
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VERSION = "version"
|
||||
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_HOST): cv.string, vol.Required(CONF_VERSION): cv.positive_int}
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Greenwave Reality Platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
tokenfilename = hass.config.path(".greenwave")
|
||||
if config.get(CONF_VERSION) == 3:
|
||||
if os.path.exists(tokenfilename):
|
||||
with open(tokenfilename, encoding="utf8") as tokenfile:
|
||||
token = tokenfile.read()
|
||||
else:
|
||||
try:
|
||||
token = greenwave.grab_token(host, "hass", "homeassistant")
|
||||
except PermissionError:
|
||||
_LOGGER.error("The Gateway Is Not In Sync Mode")
|
||||
raise
|
||||
with open(tokenfilename, "w+", encoding="utf8") as tokenfile:
|
||||
tokenfile.write(token)
|
||||
else:
|
||||
token = None
|
||||
bulbs = greenwave.grab_bulbs(host, token)
|
||||
add_entities(
|
||||
GreenwaveLight(device, host, token, GatewayData(host, token))
|
||||
for device in bulbs.values()
|
||||
)
|
||||
|
||||
|
||||
class GreenwaveLight(LightEntity):
|
||||
"""Representation of an Greenwave Reality Light."""
|
||||
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(self, light, host, token, gatewaydata):
|
||||
"""Initialize a Greenwave Reality Light."""
|
||||
self._did = int(light["did"])
|
||||
self._attr_name = light["name"]
|
||||
self._attr_is_on = bool(int(light["state"]))
|
||||
self._attr_brightness = greenwave.hass_brightness(light)
|
||||
self._host = host
|
||||
self._attr_available = greenwave.check_online(light)
|
||||
self._token = token
|
||||
self._gatewaydata = gatewaydata
|
||||
|
||||
@override
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100)
|
||||
greenwave.set_brightness(self._host, self._did, temp_brightness, self._token)
|
||||
greenwave.turn_on(self._host, self._did, self._token)
|
||||
|
||||
@override
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
greenwave.turn_off(self._host, self._did, self._token)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Fetch new state data for this light."""
|
||||
self._gatewaydata.update()
|
||||
bulbs = self._gatewaydata.greenwave
|
||||
|
||||
self._attr_is_on = bool(int(bulbs[self._did]["state"]))
|
||||
self._attr_brightness = greenwave.hass_brightness(bulbs[self._did])
|
||||
self._attr_available = greenwave.check_online(bulbs[self._did])
|
||||
self._attr_name = bulbs[self._did]["name"]
|
||||
|
||||
|
||||
class GatewayData:
|
||||
"""Handle Gateway data and limit updates."""
|
||||
|
||||
def __init__(self, host, token):
|
||||
"""Initialize the data object."""
|
||||
self._host = host
|
||||
self._token = token
|
||||
self._greenwave = greenwave.grab_bulbs(host, token)
|
||||
|
||||
@property
|
||||
def greenwave(self):
|
||||
"""Return Gateway API object."""
|
||||
return self._greenwave
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the gateway."""
|
||||
self._greenwave = greenwave.grab_bulbs(self._host, self._token)
|
||||
return self._greenwave
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "greenwave",
|
||||
"name": "Greenwave Reality",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/greenwave",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greenwavereality"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["greenwavereality==0.5.1"]
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class SupervisorJobs:
|
||||
# We catch all errors to prevent an error in one from stopping the others
|
||||
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
|
||||
try:
|
||||
return subscription.event_callback(match)
|
||||
subscription.event_callback(match)
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error(
|
||||
"Error encountered processing Supervisor Job (%s %s %s) - %s",
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.const import EntityCategory, UnitOfRatio, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
@@ -67,7 +67,7 @@ BSH_PROGRAM_SENSORS = (
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
translation_key="program_progress",
|
||||
appliance_types=APPLIANCES_WITH_PROGRAMS,
|
||||
),
|
||||
@@ -158,6 +158,7 @@ SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.13.1"]
|
||||
"requirements": ["homematicip==2.13.2"]
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Support for sending data to Logentries webhook endpoint."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_TOKEN, EVENT_STATE_CHANGED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "logentries"
|
||||
|
||||
DEFAULT_HOST = "https://webhook.logentries.com/noformat/logs/"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_TOKEN): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Logentries component."""
|
||||
conf = config[DOMAIN]
|
||||
token = conf.get(CONF_TOKEN)
|
||||
le_wh = f"{DEFAULT_HOST}{token}"
|
||||
|
||||
def logentries_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Logentries."""
|
||||
if (state := event.data.get("new_state")) is None:
|
||||
return
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
json_body = [
|
||||
{
|
||||
"domain": state.domain,
|
||||
"entity_id": state.object_id,
|
||||
"attributes": dict(state.attributes),
|
||||
"time": str(event.time_fired),
|
||||
"value": _state,
|
||||
}
|
||||
]
|
||||
try:
|
||||
payload = {"host": le_wh, "event": json_body}
|
||||
requests.post(le_wh, data=json.dumps(payload), timeout=10)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.exception("Error sending to Logentries")
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener)
|
||||
|
||||
return True
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"domain": "logentries",
|
||||
"name": "Logentries",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/logentries",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "legacy"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Support for Mycroft AI."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "mycroft"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Mycroft component."""
|
||||
hass.data[DOMAIN] = config[DOMAIN][CONF_HOST]
|
||||
discovery.load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config)
|
||||
return True
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "mycroft",
|
||||
"name": "Mycroft",
|
||||
"codeowners": [],
|
||||
"disabled": "Dependencies not compatible with the new pip resolver",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mycroft",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["mycroftapi"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["mycroftapi==2.0"]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Mycroft AI notification platform."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from mycroftapi import MycroftAPI
|
||||
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> MycroftNotificationService:
|
||||
"""Get the Mycroft notification service."""
|
||||
return MycroftNotificationService(hass.data[DOMAIN])
|
||||
|
||||
|
||||
class MycroftNotificationService(BaseNotificationService):
|
||||
"""The Mycroft Notification Service."""
|
||||
|
||||
def __init__(self, mycroft_ip: str) -> None:
|
||||
"""Initialize the service."""
|
||||
self.mycroft_ip = mycroft_ip
|
||||
|
||||
@override
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message mycroft to speak on instance."""
|
||||
|
||||
text = message
|
||||
mycroft = MycroftAPI(self.mycroft_ip)
|
||||
if mycroft is not None:
|
||||
mycroft.speak_text(text)
|
||||
else:
|
||||
_LOGGER.warning("Could not reach this instance of mycroft")
|
||||
@@ -365,5 +365,7 @@ async def create_rexel_client(
|
||||
gateway_id=entry.data[CONF_GATEWAY_ID],
|
||||
),
|
||||
session=async_create_clientsession(hass),
|
||||
settings=OverkizClientSettings(action_queue=ActionQueueSettings()),
|
||||
settings=OverkizClientSettings(
|
||||
action_queue=ActionQueueSettings(), default_rts_command_duration=0
|
||||
),
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ from homeassistant.util.event_type import EventType
|
||||
# startup
|
||||
from . import (
|
||||
backup, # noqa: F401
|
||||
entity_options,
|
||||
entity_registry,
|
||||
websocket_api,
|
||||
)
|
||||
@@ -42,6 +43,7 @@ from .const import ( # noqa: F401
|
||||
SupportedDialect,
|
||||
)
|
||||
from .core import Recorder
|
||||
from .entity_options import is_entity_recorded # noqa: F401
|
||||
from .services import async_setup_services
|
||||
from .tasks import AddRecorderPlatformTask
|
||||
from .util import get_instance
|
||||
@@ -125,15 +127,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Check if an entity is being recorded.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
instance = get_instance(hass)
|
||||
return instance.entity_filter is None or instance.entity_filter(entity_id)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the recorder."""
|
||||
conf = config[DOMAIN]
|
||||
@@ -167,6 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
get_instance.cache_clear()
|
||||
entity_registry.async_setup(hass)
|
||||
entity_options.async_setup(hass)
|
||||
instance.async_initialize()
|
||||
instance.async_register()
|
||||
instance.start()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Control recorder entity options."""
|
||||
|
||||
import dataclasses
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .util import get_instance
|
||||
|
||||
|
||||
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Check if an entity is being recorded.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
instance = get_instance(hass)
|
||||
return instance.entity_filter is None or instance.entity_filter(entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the recorder entity options."""
|
||||
websocket_api.async_register_command(hass, ws_get_entity_options)
|
||||
|
||||
|
||||
class EntityRecordingDisabler(StrEnum):
|
||||
"""What disabled recording of an entity."""
|
||||
|
||||
USER = "user"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RecorderEntityOptions:
|
||||
"""Recorder options for an entity."""
|
||||
|
||||
recording_disabled_by: EntityRecordingDisabler | None = None
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""Return a JSON serializable representation for storage."""
|
||||
return {
|
||||
"recording_disabled_by": self.recording_disabled_by,
|
||||
}
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "recorder/entity_options/get",
|
||||
vol.Required("entity_id"): cv.strict_entity_id,
|
||||
}
|
||||
)
|
||||
@callback
|
||||
def ws_get_entity_options(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Get recorder settings for a single entity."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
recording_disabled = (
|
||||
None if is_entity_recorded(hass, entity_id) else EntityRecordingDisabler.USER
|
||||
)
|
||||
|
||||
options = RecorderEntityOptions(recording_disabled_by=recording_disabled)
|
||||
connection.send_result(msg["id"], options.to_json())
|
||||
@@ -321,6 +321,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
|
||||
options=[
|
||||
"always",
|
||||
"delayed",
|
||||
"delegated",
|
||||
"scheduled",
|
||||
],
|
||||
value_lambda=_get_charging_settings_mode_formatted,
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
"state": {
|
||||
"always": "Always",
|
||||
"delayed": "Delayed",
|
||||
"delegated": "Delegated",
|
||||
"scheduled": "Scheduled"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -286,7 +286,10 @@ async def register_callbacks(
|
||||
return async_camera_wake
|
||||
|
||||
host.api.baichuan.register_callback(
|
||||
"privacy_mode_change", async_privacy_mode_change, 623
|
||||
"privacy_mode_change_623", async_privacy_mode_change, 623
|
||||
)
|
||||
host.api.baichuan.register_callback(
|
||||
"privacy_mode_change_574", async_privacy_mode_change, 574
|
||||
)
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
@@ -306,7 +309,8 @@ async def async_unload_entry(
|
||||
|
||||
await host.stop()
|
||||
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change")
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change_623")
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change_574")
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
|
||||
|
||||
@@ -75,6 +75,7 @@ LIGHT_ENTITIES = (
|
||||
ReolinkLightEntityDescription(
|
||||
key="status_led",
|
||||
cmd_key="GetPowerLed",
|
||||
cmd_id=208,
|
||||
translation_key="status_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "power_led"),
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.21.2"]
|
||||
"requirements": ["reolink-aio==0.21.3"]
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ NUMBER_ENTITIES = (
|
||||
key="volume",
|
||||
cmd_key="GetAudioCfg",
|
||||
translation_key="volume",
|
||||
cmd_id=264,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
@@ -206,6 +207,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="volume_speak",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="volume_speak",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -218,6 +220,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="volume_doorbell",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="volume_doorbell",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -269,6 +272,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="pir_sensitivity",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -281,6 +285,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="pir_interval",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_interval",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -296,6 +301,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_face_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_face_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -310,6 +316,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_person_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_person_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -324,6 +331,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_vehicle_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_vehicle_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -338,6 +346,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_sensitivity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_non_motor_vehicle_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -355,6 +364,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_package_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_package_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -369,6 +379,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_pet_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -385,6 +396,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_animal_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -411,6 +423,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_face_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_face_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -428,6 +441,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_person_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_person_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -445,6 +459,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_non_motor_vehicle_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -464,6 +479,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_vehicle_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -481,6 +497,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_package_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_package_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -498,6 +515,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_pet_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -517,6 +535,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_animal_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
|
||||
@@ -185,6 +185,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="status_led",
|
||||
cmd_key="GetPowerLed",
|
||||
cmd_id=208,
|
||||
translation_key="doorbell_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=lambda api, ch: api.doorbell_led_list(ch),
|
||||
@@ -232,6 +233,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_frame_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_frame_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -244,6 +246,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_frame_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_frame_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -256,6 +259,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_bit_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_bit_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -268,6 +272,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_bit_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_bit_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -280,6 +285,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_encoding",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_encoding",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -291,6 +297,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_encoding",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_encoding",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -316,6 +323,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="post_rec_time",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=54,
|
||||
translation_key="post_rec_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -340,6 +348,7 @@ HOST_SELECT_ENTITIES = (
|
||||
ReolinkHostSelectEntityDescription(
|
||||
key="packing_time",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=54,
|
||||
translation_key="packing_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
|
||||
@@ -74,6 +74,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="ir_lights",
|
||||
cmd_key="GetIrLights",
|
||||
cmd_id=208,
|
||||
translation_key="ir_lights",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "ir_lights"),
|
||||
@@ -83,6 +84,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="record_audio",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="record_audio",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "audio"),
|
||||
@@ -92,6 +94,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="siren_on_event",
|
||||
cmd_key="GetAudioAlarm",
|
||||
cmd_id=232,
|
||||
translation_key="siren_on_event",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "siren"),
|
||||
@@ -136,6 +139,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="email",
|
||||
cmd_key="GetEmail",
|
||||
cmd_id=217,
|
||||
translation_key="email",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "email") and api.is_nvr,
|
||||
@@ -145,6 +149,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="ftp_upload",
|
||||
cmd_key="GetFtp",
|
||||
cmd_id=70,
|
||||
translation_key="ftp_upload",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "ftp") and api.is_nvr,
|
||||
@@ -163,6 +168,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="record",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=81,
|
||||
translation_key="record",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "rec_enable") and api.is_nvr,
|
||||
@@ -200,6 +206,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="doorbell_button_sound",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="doorbell_button_sound",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "doorbell_button_sound"),
|
||||
@@ -209,6 +216,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="pir_enabled",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_enabled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -219,6 +227,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="pir_reduce_alarm",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_reduce_alarm",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -260,6 +269,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="email",
|
||||
cmd_key="GetEmail",
|
||||
cmd_id=217,
|
||||
translation_key="email",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "email") and not api.is_hub,
|
||||
@@ -269,6 +279,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="ftp_upload",
|
||||
cmd_key="GetFtp",
|
||||
cmd_id=70,
|
||||
translation_key="ftp_upload",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "ftp") and not api.is_hub,
|
||||
@@ -287,6 +298,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="record",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=81,
|
||||
translation_key="record",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "rec_enable") and not api.is_hub,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==5.14.2",
|
||||
"python-roborock==5.21.0",
|
||||
"vacuum-map-parser-roborock==0.1.5"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class RoborockNumberDescription(NumberEntityDescription):
|
||||
trait: Callable[[PropertiesApi], Any | None]
|
||||
"""Function to determine if number entity is supported by the device."""
|
||||
|
||||
get_value: Callable[[Any], float]
|
||||
get_value: Callable[[Any], float | None]
|
||||
"""Function to get the value from the trait."""
|
||||
|
||||
set_value: Callable[[Any, float], Coroutine[Any, Any, None]]
|
||||
@@ -51,7 +51,9 @@ NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
trait=lambda api: api.sound_volume,
|
||||
get_value=lambda trait: float(trait.volume),
|
||||
get_value=lambda trait: (
|
||||
float(trait.volume) if trait.volume is not None else None
|
||||
),
|
||||
set_value=lambda trait, value: trait.set_volume(int(value)),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -37,7 +37,7 @@ class RoborockTimeDescription(TimeEntityDescription):
|
||||
trait: Callable[[Any], Any | None]
|
||||
"""Function to determine if time entity is supported by the device."""
|
||||
|
||||
get_value: Callable[[Any], datetime.time]
|
||||
get_value: Callable[[Any], datetime.time | None]
|
||||
"""Function to get the value from the trait."""
|
||||
|
||||
update_value: Callable[[Any, datetime.time], Coroutine[Any, Any, None]]
|
||||
@@ -58,9 +58,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=trait.end_minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.start_hour, minute=trait.start_minute
|
||||
),
|
||||
get_value=lambda trait: trait.start_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
RoborockTimeDescription(
|
||||
@@ -76,9 +74,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=desired_time.minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.end_hour, minute=trait.end_minute
|
||||
),
|
||||
get_value=lambda trait: trait.end_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
RoborockTimeDescription(
|
||||
@@ -94,9 +90,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=trait.end_minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.start_hour, minute=trait.start_minute
|
||||
),
|
||||
get_value=lambda trait: trait.start_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -113,9 +107,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=desired_time.minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.end_hour, minute=trait.end_minute
|
||||
),
|
||||
get_value=lambda trait: trait.end_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Config flow to configure StarLine component."""
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
from typing import override
|
||||
|
||||
from starline import StarlineAuth
|
||||
import voluptuous as vol
|
||||
@@ -192,18 +192,13 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Authenticate application."""
|
||||
try:
|
||||
|
||||
def _get_app_token() -> str:
|
||||
if TYPE_CHECKING:
|
||||
assert self._app_id is not None
|
||||
assert self._app_secret is not None
|
||||
|
||||
app_code = self._auth.get_app_code(self._app_id, self._app_secret)
|
||||
return self._auth.get_app_token(
|
||||
self._app_id, self._app_secret, app_code
|
||||
)
|
||||
|
||||
self._app_token = await self.hass.async_add_executor_job(_get_app_token)
|
||||
self._app_code = await self.hass.async_add_executor_job(
|
||||
self._auth.get_app_code, self._app_id, self._app_secret
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
self._app_token = await self.hass.async_add_executor_job(
|
||||
self._auth.get_app_token, self._app_id, self._app_secret, self._app_code
|
||||
)
|
||||
return self._async_form_auth_user(error)
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error("Error auth StarLine: %s", err)
|
||||
|
||||
@@ -85,34 +85,18 @@ def sun(
|
||||
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
|
||||
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
|
||||
|
||||
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
|
||||
after_sunrise = sunrise is not None and today > dt_util.as_local(sunrise).date()
|
||||
if after_sunrise and has_sunrise_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
|
||||
|
||||
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
|
||||
after_sunset = sunset is not None and today > dt_util.as_local(sunset).date()
|
||||
if after_sunset and has_sunset_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
|
||||
|
||||
# Special case: before sunrise OR after sunset
|
||||
# This will handle the very rare case in the polar region when the sun rises/sets
|
||||
# but does not set/rise.
|
||||
# However this entire condition does not handle those full days of darkness
|
||||
# or light, the following should be used instead:
|
||||
#
|
||||
# condition:
|
||||
# condition: state
|
||||
# entity_id: sun.sun
|
||||
# state: 'above_horizon' (or 'below_horizon')
|
||||
#
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
# A missing sunrise/sunset means the sun doesn't rise/set on this day, which
|
||||
# happens in polar regions.
|
||||
if sunrise is None and has_sunrise_condition:
|
||||
# There is no sunrise today
|
||||
condition_trace_set_result(False, message="no sunrise today")
|
||||
@@ -123,6 +107,16 @@ def sun(
|
||||
condition_trace_set_result(False, message="no sunset today")
|
||||
return False
|
||||
|
||||
# "before: sunrise" combined with "after: sunset" describes the dark period
|
||||
# around midnight, so it is evaluated as an OR (true before sunrise or after
|
||||
# sunset) rather than the usual AND of the two bounds.
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
if before == SUN_EVENT_SUNRISE:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.11.1"]
|
||||
"requirements": ["switchbot-api==2.12.0"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"state": {
|
||||
"cool": "mdi:fan-speed-2",
|
||||
"full_speed": "mdi:fan-speed-3",
|
||||
"low_power": "mdi:fan-chevron-down",
|
||||
"quiet": "mdi:fan-speed-1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.10.0"],
|
||||
"requirements": ["py-synologydsm-api==2.10.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:Basic:1",
|
||||
|
||||
@@ -15,6 +15,7 @@ from .coordinator import SynologyDSMCentralUpdateCoordinator, SynologyDSMConfigE
|
||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||
|
||||
FAN_SPEED_MAP = {
|
||||
FanSpeed.QUIET_STOP: "low_power",
|
||||
FanSpeed.QUIET: "quiet",
|
||||
FanSpeed.COOL: "cool",
|
||||
FanSpeed.FULL: "full_speed",
|
||||
@@ -47,7 +48,6 @@ class SynologyDSMFanSpeedMode(
|
||||
):
|
||||
"""Represent a Synology DSM fan speed mode select entity."""
|
||||
|
||||
_attr_options = list(FAN_SPEED_MAP.values())
|
||||
entity_description: SynologyDSMSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -62,6 +62,11 @@ class SynologyDSMFanSpeedMode(
|
||||
translation_key="fan_speed_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
self._attr_options = [
|
||||
val
|
||||
for fs, val in FAN_SPEED_MAP.items()
|
||||
if fs in api.dsm.hardware.supported_fan_speeds
|
||||
]
|
||||
super().__init__(api, coordinator, description)
|
||||
|
||||
@property
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"state": {
|
||||
"cool": "Cool mode",
|
||||
"full_speed": "Full-speed mode",
|
||||
"low_power": "Low-Power mode",
|
||||
"quiet": "Quiet mode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,12 +67,13 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
otp = user_input["otp"]
|
||||
try:
|
||||
refresh_token = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI.submit_otp, self.phone, otp
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
api = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI, refresh_token
|
||||
|
||||
def _submit_otp_and_create_api() -> tuple[str, Tami4EdgeAPI]:
|
||||
refresh_token = Tami4EdgeAPI.submit_otp(self.phone, otp)
|
||||
return refresh_token, Tami4EdgeAPI(refresh_token)
|
||||
|
||||
refresh_token, api = await self.hass.async_add_executor_job(
|
||||
_submit_otp_and_create_api
|
||||
)
|
||||
except exceptions.OTPFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The thermoworks_smoke component."""
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "thermoworks_smoke",
|
||||
"name": "ThermoWorks Smoke",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it creates an unresolvable dependency conflict.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["thermoworks_smoke"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["stringcase==1.2.0", "thermoworks-smoke==0.1.8"]
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
"""Support for getting the state of a Thermoworks Smoke Thermometer.
|
||||
|
||||
Requires Smoke Gateway Wifi with an internet connection.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from requests import RequestException
|
||||
from requests.exceptions import HTTPError
|
||||
from stringcase import camelcase
|
||||
import thermoworks_smoke
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
CONF_EMAIL,
|
||||
CONF_EXCLUDE,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_PASSWORD,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import snakecase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROBE_1 = "probe1"
|
||||
PROBE_2 = "probe2"
|
||||
PROBE_1_MIN = "probe1_min"
|
||||
PROBE_1_MAX = "probe1_max"
|
||||
PROBE_2_MIN = "probe2_min"
|
||||
PROBE_2_MAX = "probe2_max"
|
||||
BATTERY_LEVEL = "battery"
|
||||
FIRMWARE = "firmware"
|
||||
|
||||
SERIAL_REGEX = "^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"
|
||||
|
||||
# map types to labels
|
||||
SENSOR_TYPES = {
|
||||
PROBE_1: "Probe 1",
|
||||
PROBE_2: "Probe 2",
|
||||
PROBE_1_MIN: "Probe 1 Min",
|
||||
PROBE_1_MAX: "Probe 1 Max",
|
||||
PROBE_2_MIN: "Probe 2 Min",
|
||||
PROBE_2_MAX: "Probe 2 Max",
|
||||
}
|
||||
|
||||
# exclude these keys from thermoworks data
|
||||
EXCLUDE_KEYS = [FIRMWARE]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[PROBE_1, PROBE_2]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
),
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.matches_regex(SERIAL_REGEX)]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the thermoworks sensor."""
|
||||
|
||||
email = config[CONF_EMAIL]
|
||||
password = config[CONF_PASSWORD]
|
||||
monitored_variables = config[CONF_MONITORED_CONDITIONS]
|
||||
excluded = config[CONF_EXCLUDE]
|
||||
|
||||
try:
|
||||
mgr = thermoworks_smoke.initialize_app(email, password, True, excluded)
|
||||
except HTTPError as error:
|
||||
msg = f"{error.strerror}"
|
||||
if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg:
|
||||
_LOGGER.error("Invalid email and password combination")
|
||||
else:
|
||||
_LOGGER.error(msg)
|
||||
else:
|
||||
add_entities(
|
||||
(
|
||||
ThermoworksSmokeSensor(variable, serial, mgr)
|
||||
for serial in mgr.serials()
|
||||
for variable in monitored_variables
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class ThermoworksSmokeSensor(SensorEntity):
|
||||
"""Implementation of a thermoworks smoke sensor."""
|
||||
|
||||
def __init__(self, sensor_type, serial, mgr):
|
||||
"""Initialize the sensor."""
|
||||
self.type = sensor_type
|
||||
self.serial = serial
|
||||
self.mgr = mgr
|
||||
self._attr_name = f"{mgr.name(serial)} {SENSOR_TYPES[sensor_type]}"
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT
|
||||
self._attr_unique_id = f"{serial}-{sensor_type}"
|
||||
self._attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
self.update_unit()
|
||||
|
||||
def update_unit(self):
|
||||
"""Set the units from the data."""
|
||||
if PROBE_2 in self.type:
|
||||
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_2)
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_1)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the monitored data from firebase."""
|
||||
|
||||
try:
|
||||
values = self.mgr.data(self.serial)
|
||||
|
||||
# set state from data based on type of sensor
|
||||
self._attr_native_value = values.get(camelcase(self.type))
|
||||
|
||||
# set units
|
||||
self.update_unit()
|
||||
|
||||
# set basic attributes for all sensors
|
||||
self._attr_extra_state_attributes = {
|
||||
"time": values["time"],
|
||||
"localtime": values["localtime"],
|
||||
}
|
||||
|
||||
# set extended attributes for main probe sensors
|
||||
if self.type in (PROBE_1, PROBE_2):
|
||||
for key, val in values.items():
|
||||
# add all attributes that don't contain any probe name
|
||||
# or contain a matching probe name
|
||||
if (self.type == PROBE_1 and key.find(PROBE_2) == -1) or (
|
||||
self.type == PROBE_2 and key.find(PROBE_1) == -1
|
||||
):
|
||||
if key == BATTERY_LEVEL:
|
||||
key = ATTR_BATTERY_LEVEL
|
||||
else:
|
||||
# strip probe label and convert to snake_case
|
||||
key = snakecase(key.replace(self.type, ""))
|
||||
# add to attrs
|
||||
if key and key not in EXCLUDE_KEYS:
|
||||
self._attr_extra_state_attributes[key] = val
|
||||
# store actual unit because attributes are not converted
|
||||
self._attr_extra_state_attributes["unit_of_min_max"] = (
|
||||
self._attr_native_unit_of_measurement
|
||||
)
|
||||
|
||||
except RequestException, ValueError, KeyError:
|
||||
_LOGGER.warning("Could not update status for %s", self.name)
|
||||
@@ -44,7 +44,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["tuya_sharing"],
|
||||
"requirements": [
|
||||
"tuya-device-handlers==0.0.22",
|
||||
"tuya-device-sharing-sdk==0.2.8"
|
||||
"tuya-device-handlers==0.0.24",
|
||||
"tuya-device-sharing-sdk==0.2.10"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==14.0.0"]
|
||||
"requirements": ["uiprotect==15.3.0"]
|
||||
}
|
||||
|
||||
@@ -62,10 +62,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeraConfigEntry) -> bool
|
||||
controller = veraApi.VeraController(base_url, subscription_registry)
|
||||
|
||||
try:
|
||||
all_devices = await hass.async_add_executor_job(controller.get_devices)
|
||||
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
all_scenes = await hass.async_add_executor_job(controller.get_scenes)
|
||||
def _get_devices_and_scenes():
|
||||
"""Get devices and scenes from the Vera controller."""
|
||||
return controller.get_devices(), controller.get_scenes()
|
||||
|
||||
all_devices, all_scenes = await hass.async_add_executor_job(
|
||||
_get_devices_and_scenes
|
||||
)
|
||||
except RequestException as exception:
|
||||
# There was a network related error connecting to the Vera controller.
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
|
||||
@@ -145,9 +145,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (
|
||||
exceptions.CannotConnect,
|
||||
exceptions.AlreadyLogged,
|
||||
exceptions.GenericLoginError,
|
||||
exceptions.VodafoneError,
|
||||
JSONDecodeError,
|
||||
) as err:
|
||||
if isinstance(err, JSONDecodeError):
|
||||
|
||||
@@ -149,7 +149,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_DISPLAY_ORIENTATION,
|
||||
attr_name=ATTR_DISPLAY_ORIENTATION,
|
||||
name="Display Orientation",
|
||||
options_map={
|
||||
"Portrait": "Forward",
|
||||
"LandscapeLeft": "Left",
|
||||
@@ -165,7 +164,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_MODE,
|
||||
attr_name=ATTR_MODE,
|
||||
name="Mode",
|
||||
set_method="set_mode",
|
||||
set_method_error_message="Setting the mode of the fan failed.",
|
||||
icon="mdi:fan",
|
||||
@@ -176,7 +174,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_LED_BRIGHTNESS,
|
||||
attr_name=ATTR_LED_BRIGHTNESS,
|
||||
name="Led Brightness",
|
||||
set_method="set_led_brightness",
|
||||
set_method_error_message="Setting the led brightness failed.",
|
||||
icon="mdi:brightness-6",
|
||||
@@ -187,7 +184,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_PTC_LEVEL,
|
||||
attr_name=ATTR_PTC_LEVEL,
|
||||
name="Auxiliary Heat Level",
|
||||
set_method="set_ptc_level",
|
||||
set_method_error_message="Setting the ptc level failed.",
|
||||
icon="mdi:fire-circle",
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
},
|
||||
"select": {
|
||||
"airpurifier_mode": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"favorite": "Favorite",
|
||||
@@ -145,6 +146,7 @@
|
||||
}
|
||||
},
|
||||
"display_orientation": {
|
||||
"name": "Display orientation",
|
||||
"state": {
|
||||
"forward": "Forward",
|
||||
"left": "Left",
|
||||
@@ -152,6 +154,7 @@
|
||||
}
|
||||
},
|
||||
"led_brightness": {
|
||||
"name": "LED brightness",
|
||||
"state": {
|
||||
"bright": "Bright",
|
||||
"dim": "Dim",
|
||||
@@ -159,6 +162,7 @@
|
||||
}
|
||||
},
|
||||
"ptc_level": {
|
||||
"name": "Auxiliary heat level",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
|
||||
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -590,12 +590,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"aten_pe": {
|
||||
"name": "ATEN Rack PDU",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"atlanticcityelectric": {
|
||||
"name": "Atlantic City Electric",
|
||||
"integration_type": "virtual",
|
||||
@@ -1489,12 +1483,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"dovado": {
|
||||
"name": "Dovado",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"downloader": {
|
||||
"name": "Downloader",
|
||||
"integration_type": "service",
|
||||
@@ -2730,12 +2718,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"greenwave": {
|
||||
"name": "Greenwave Reality",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"growatt_server": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
@@ -3947,12 +3929,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"logentries": {
|
||||
"name": "Logentries",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"logitech": {
|
||||
"name": "Logitech",
|
||||
"integrations": {
|
||||
@@ -4587,12 +4563,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"mycroft": {
|
||||
"name": "Mycroft",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"myneomitis": {
|
||||
"name": "MyNeomitis",
|
||||
"integration_type": "hub",
|
||||
@@ -7207,12 +7177,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"thermoworks_smoke": {
|
||||
"name": "ThermoWorks Smoke",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"thethingsnetwork": {
|
||||
"name": "The Things Network",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -740,7 +740,7 @@ def _get_exposed_entities(
|
||||
|
||||
if include_state and (
|
||||
attributes := {
|
||||
attr_name: (
|
||||
str(attr_name): (
|
||||
str(attr_value)
|
||||
if isinstance(attr_value, (Enum, Decimal, int))
|
||||
else attr_value
|
||||
|
||||
@@ -35,12 +35,12 @@ file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.23.1
|
||||
habluetooth==6.25.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.8.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260624.0
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-frontend==20260624.1
|
||||
home-assistant-intents==2026.6.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
@@ -71,7 +71,7 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.21
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.4.1
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260624.0"
|
||||
FRONTEND_VERSION: Final[str] = "20260624.1"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.7.0.dev0"
|
||||
version = "2026.7.0b2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -77,7 +77,7 @@ dependencies = [
|
||||
"uv==0.11.21",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.7.0",
|
||||
"voluptuous-openapi==0.3.0",
|
||||
"voluptuous-openapi==0.4.1",
|
||||
"yarl==1.24.2",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.150.0",
|
||||
|
||||
Generated
+2
-2
@@ -27,7 +27,7 @@ ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.8.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-intents==2026.6.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
infrared-protocols==6.3.0
|
||||
@@ -56,7 +56,7 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.21
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.4.1
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
|
||||
Generated
+13
-19
@@ -193,7 +193,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==14.1.3
|
||||
aioamazondevices==14.1.8
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -583,9 +583,6 @@ asyncsleepiq==1.7.1
|
||||
# homeassistant.components.sftp_storage
|
||||
asyncssh==2.21.0
|
||||
|
||||
# homeassistant.components.aten_pe
|
||||
# atenpdu==0.3.6
|
||||
|
||||
# homeassistant.components.aurora
|
||||
auroranoaa==0.0.5
|
||||
|
||||
@@ -1179,9 +1176,6 @@ greeneye_monitor==3.0.3
|
||||
# homeassistant.components.green_planet_energy
|
||||
greenplanet-energy-api==0.1.10
|
||||
|
||||
# homeassistant.components.greenwave
|
||||
greenwavereality==0.5.1
|
||||
|
||||
# homeassistant.components.pure_energie
|
||||
gridnet==5.0.1
|
||||
|
||||
@@ -1219,7 +1213,7 @@ ha-xthings-cloud==1.0.5
|
||||
habiticalib==0.4.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==6.23.1
|
||||
habluetooth==6.25.1
|
||||
|
||||
# homeassistant.components.hanna
|
||||
hanna-cloud==0.0.7
|
||||
@@ -1272,10 +1266,10 @@ hole==0.9.2
|
||||
holidays==0.99
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260624.0
|
||||
home-assistant-frontend==20260624.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.6.1
|
||||
home-assistant-intents==2026.6.24
|
||||
|
||||
# homeassistant.components.homekit
|
||||
homekit-audio-proxy==1.2.1
|
||||
@@ -1284,7 +1278,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.5
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.13.1
|
||||
homematicip==2.13.2
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
@@ -1948,7 +1942,7 @@ py-schluter==0.1.7
|
||||
py-sucks==0.9.11
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.10.0
|
||||
py-synologydsm-api==2.10.1
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.3.0
|
||||
@@ -2018,7 +2012,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.2.2
|
||||
pyanglianwater==3.2.3
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -2730,7 +2724,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==5.14.2
|
||||
python-roborock==5.21.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
@@ -2899,7 +2893,7 @@ renault-api==0.5.12
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.21.2
|
||||
reolink-aio==0.21.3
|
||||
|
||||
# homeassistant.components.radio_frequency
|
||||
rf-protocols==4.3.0
|
||||
@@ -3114,7 +3108,7 @@ surepy==0.9.0
|
||||
swisshydrodata==0.1.0
|
||||
|
||||
# homeassistant.components.switchbot_cloud
|
||||
switchbot-api==2.11.1
|
||||
switchbot-api==2.12.0
|
||||
|
||||
# homeassistant.components.synology_srm
|
||||
synology-srm==0.2.0
|
||||
@@ -3221,10 +3215,10 @@ ttls==1.8.3
|
||||
ttn_client==1.3.0
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-handlers==0.0.22
|
||||
tuya-device-handlers==0.0.24
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.2.8
|
||||
tuya-device-sharing-sdk==0.2.10
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==3.0.0
|
||||
@@ -3245,7 +3239,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==14.0.0
|
||||
uiprotect==15.3.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.6.1
|
||||
|
||||
@@ -165,7 +165,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"asterisk_mbox",
|
||||
"asuswrt",
|
||||
"atag",
|
||||
"aten_pe",
|
||||
"atome",
|
||||
"august",
|
||||
"aurora",
|
||||
@@ -274,7 +273,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"doods",
|
||||
"doorbird",
|
||||
"dormakaba_dkey",
|
||||
"dovado",
|
||||
"downloader",
|
||||
"dremel_3d_printer",
|
||||
"drop_connect",
|
||||
@@ -420,7 +418,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"graphite",
|
||||
"gree",
|
||||
"greeneye_monitor",
|
||||
"greenwave",
|
||||
"group",
|
||||
"gtfs",
|
||||
"guardian",
|
||||
@@ -548,7 +545,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"local_todo",
|
||||
"location",
|
||||
"locative",
|
||||
"logentries",
|
||||
"logi_circle",
|
||||
"london_air",
|
||||
"london_underground",
|
||||
@@ -622,7 +618,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"mullvad",
|
||||
"mutesync",
|
||||
"mvglive",
|
||||
"mycroft",
|
||||
"myq",
|
||||
"mysensors",
|
||||
"mystrom",
|
||||
@@ -914,7 +909,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"tesla_wall_connector",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thermoworks_smoke",
|
||||
"thethingsnetwork",
|
||||
"thingspeak",
|
||||
"thinkingcleaner",
|
||||
@@ -1114,7 +1108,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"asterisk_mbox",
|
||||
"asuswrt",
|
||||
"atag",
|
||||
"aten_pe",
|
||||
"atome",
|
||||
"august",
|
||||
"aurora",
|
||||
@@ -1226,7 +1219,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"doods",
|
||||
"doorbird",
|
||||
"dormakaba_dkey",
|
||||
"dovado",
|
||||
"downloader",
|
||||
"dremel_3d_printer",
|
||||
"drop_connect",
|
||||
@@ -1379,7 +1371,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"graphite",
|
||||
"gree",
|
||||
"greeneye_monitor",
|
||||
"greenwave",
|
||||
"group",
|
||||
"gtfs",
|
||||
"guardian",
|
||||
@@ -1514,7 +1505,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"local_todo",
|
||||
"location",
|
||||
"locative",
|
||||
"logentries",
|
||||
"logi_circle",
|
||||
"london_air",
|
||||
"london_underground",
|
||||
@@ -1589,7 +1579,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"mullvad",
|
||||
"mutesync",
|
||||
"mvglive",
|
||||
"mycroft",
|
||||
"myq",
|
||||
"mysensors",
|
||||
"mystrom",
|
||||
@@ -1898,7 +1887,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"tesla_wall_connector",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thermoworks_smoke",
|
||||
"thethingsnetwork",
|
||||
"thingspeak",
|
||||
"thinkingcleaner",
|
||||
|
||||
@@ -107,8 +107,8 @@ async def test_alexa_unique_id_migration(
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
DOMAIN,
|
||||
unique_id=f"{TEST_DEVICE_1_SN}-do_not_disturb",
|
||||
device_id=device.id,
|
||||
config_entry=mock_config_entry,
|
||||
@@ -145,8 +145,8 @@ async def test_alexa_dnd_group_removal(
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
DOMAIN,
|
||||
unique_id=f"{TEST_DEVICE_1_SN}-do_not_disturb",
|
||||
device_id=device.id,
|
||||
config_entry=mock_config_entry,
|
||||
@@ -184,8 +184,8 @@ async def test_alexa_unsupported_notification_sensor_removal(
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
unique_id=f"{TEST_DEVICE_1_SN}-Timer",
|
||||
device_id=device.id,
|
||||
config_entry=mock_config_entry,
|
||||
|
||||
@@ -853,7 +853,7 @@
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'Sorry, I am not aware of any area called Are the',
|
||||
'speech': 'Sorry, I am not aware of any device called Are the',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -902,7 +902,7 @@
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'Sorry, I am not aware of any area called Are the',
|
||||
'speech': 'Sorry, I am not aware of any device called Are the',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from unittest.mock import ANY, Mock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import automation, input_boolean, script
|
||||
from homeassistant.components.automation import (
|
||||
@@ -1930,6 +1931,187 @@ async def test_automation_with_error_in_script_2(
|
||||
assert "string value is None" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error", "expect_traceback"),
|
||||
[
|
||||
(
|
||||
HomeAssistantError("boom"),
|
||||
"Error while executing automation automation.hello: boom",
|
||||
False,
|
||||
),
|
||||
(
|
||||
vol.Invalid("not valid"),
|
||||
"Error while executing automation automation.hello: not valid",
|
||||
False,
|
||||
),
|
||||
(
|
||||
ValueError("unexpected"),
|
||||
"Unexpected error while executing automation automation.hello",
|
||||
True,
|
||||
),
|
||||
],
|
||||
ids=["home_assistant_error", "voluptuous_invalid", "unexpected_exception"],
|
||||
)
|
||||
async def test_automation_with_error_in_action_script(
|
||||
hass: HomeAssistant,
|
||||
calls: list[ServiceCall],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
side_effect: Exception,
|
||||
expected_error: str,
|
||||
expect_traceback: bool,
|
||||
) -> None:
|
||||
"""Test errors raised while running the action script are handled and traced."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "hello",
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": {"action": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.script.Script.async_run",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 0
|
||||
assert expected_error in caplog.text
|
||||
# A HomeAssistantError/voluptuous error is logged without a traceback, an
|
||||
# unexpected error is logged with a traceback.
|
||||
assert ("Traceback" in caplog.text) is expect_traceback
|
||||
|
||||
# The error is recorded on the automation trace.
|
||||
client = await hass_ws_client()
|
||||
await client.send_json_auto_id(
|
||||
{"type": "trace/list", "domain": "automation", "item_id": "hello"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
traces = response["result"]
|
||||
assert len(traces) == 1
|
||||
assert traces[0]["error"] == str(side_effect)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error", "expect_traceback"),
|
||||
[
|
||||
(
|
||||
HomeAssistantError("boom"),
|
||||
"Error while checking conditions of automation automation.hello: boom",
|
||||
False,
|
||||
),
|
||||
(
|
||||
vol.Invalid("not valid"),
|
||||
"Error while checking conditions of automation automation.hello: not valid",
|
||||
False,
|
||||
),
|
||||
(
|
||||
ValueError("unexpected"),
|
||||
"Unexpected error while checking conditions of automation automation.hello",
|
||||
True,
|
||||
),
|
||||
],
|
||||
ids=["home_assistant_error", "voluptuous_invalid", "unexpected_exception"],
|
||||
)
|
||||
async def test_automation_with_error_in_condition(
|
||||
hass: HomeAssistant,
|
||||
calls: list[ServiceCall],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
side_effect: Exception,
|
||||
expected_error: str,
|
||||
expect_traceback: bool,
|
||||
) -> None:
|
||||
"""Test errors raised while checking conditions are handled and traced."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "hello",
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": "state",
|
||||
"entity_id": "test.entity",
|
||||
"state": "on",
|
||||
},
|
||||
"action": {"action": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.ConditionsChecker.async_check",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The action must not run when the condition check raises.
|
||||
assert len(calls) == 0
|
||||
assert expected_error in caplog.text
|
||||
# A HomeAssistantError/voluptuous error is logged without a traceback, an
|
||||
# unexpected error is logged with a traceback.
|
||||
assert ("Traceback" in caplog.text) is expect_traceback
|
||||
|
||||
# The error is recorded on the automation trace.
|
||||
client = await hass_ws_client()
|
||||
await client.send_json_auto_id(
|
||||
{"type": "trace/list", "domain": "automation", "item_id": "hello"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
traces = response["result"]
|
||||
assert len(traces) == 1
|
||||
assert traces[0]["error"] == str(side_effect)
|
||||
|
||||
|
||||
async def test_automation_with_error_in_condition_continues_after_recovery(
|
||||
hass: HomeAssistant,
|
||||
calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test the automation still runs once the condition stops raising."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": "state",
|
||||
"entity_id": "test.entity",
|
||||
"state": "on",
|
||||
},
|
||||
"action": {"action": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
hass.states.async_set("test.entity", "on")
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.ConditionsChecker.async_check",
|
||||
side_effect=HomeAssistantError("boom"),
|
||||
):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
# Without the error, the condition passes and the action runs.
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
async def test_automation_restore_last_triggered_with_initial_state(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'Sorry, I am not aware of any area called late added',
|
||||
'speech': 'Sorry, I am not aware of any device called late added',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -373,7 +373,7 @@
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'Sorry, I am not aware of any area called kitchen',
|
||||
'speech': 'Sorry, I am not aware of any device called kitchen',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -423,7 +423,7 @@
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'Sorry, I am not aware of any area called renamed',
|
||||
'speech': 'Sorry, I am not aware of any device called renamed',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -467,10 +467,11 @@
|
||||
'name': 'HassTurnOn',
|
||||
}),
|
||||
'match': True,
|
||||
'sentence_template': '<turn> on (<area> <name>|<name> [in <area>])',
|
||||
'sentence_template': '<turn> on [<the>] {name}',
|
||||
'slots': dict({
|
||||
'name': 'my cool light',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
'light.kitchen': dict({
|
||||
'matched': True,
|
||||
@@ -491,10 +492,11 @@
|
||||
'name': 'HassTurnOff',
|
||||
}),
|
||||
'match': True,
|
||||
'sentence_template': '[<turn>] (<area> <name>|<name> [in <area>]) [to] off',
|
||||
'sentence_template': '[<turn>] [<the>] {name} [to] off',
|
||||
'slots': dict({
|
||||
'name': 'my cool light',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
'light.kitchen': dict({
|
||||
'matched': True,
|
||||
@@ -520,11 +522,12 @@
|
||||
'name': 'HassTurnOn',
|
||||
}),
|
||||
'match': True,
|
||||
'sentence_template': '<turn> on [(<all>|<the>)] <light> <in> <area>',
|
||||
'sentence_template': '<turn> on [(<all>|<the>)] <light> <in> [<the>] {area}',
|
||||
'slots': dict({
|
||||
'area': 'kitchen',
|
||||
'domain': 'light',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
'light.kitchen': dict({
|
||||
'matched': True,
|
||||
@@ -542,7 +545,7 @@
|
||||
}),
|
||||
'domain': dict({
|
||||
'name': 'domain',
|
||||
'text': 'lights',
|
||||
'text': '',
|
||||
'value': 'light',
|
||||
}),
|
||||
'state': dict({
|
||||
@@ -555,12 +558,13 @@
|
||||
'name': 'HassGetState',
|
||||
}),
|
||||
'match': True,
|
||||
'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [<in_area_floor>]',
|
||||
'sentence_template': '<how_many> <light> <is> {on_off_states:state} [<in>] [<the>] {area}',
|
||||
'slots': dict({
|
||||
'area': 'kitchen',
|
||||
'domain': 'lights',
|
||||
'domain': 'light',
|
||||
'state': 'on',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
'light.kitchen': dict({
|
||||
'matched': False,
|
||||
@@ -629,11 +633,12 @@
|
||||
'name': 'HassLightSet',
|
||||
}),
|
||||
'match': True,
|
||||
'sentence_template': '[<numeric_value_set>] <name> brightness [to] <brightness>',
|
||||
'sentence_template': '[<numeric_value_set>] [<the>] {name} brightness [to] {brightness}[([ ]%)| percent]',
|
||||
'slots': dict({
|
||||
'brightness': '100',
|
||||
'name': 'test light',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
'light.demo_1234': dict({
|
||||
'matched': True,
|
||||
@@ -660,10 +665,11 @@
|
||||
'name': 'HassLightSet',
|
||||
}),
|
||||
'match': False,
|
||||
'sentence_template': '[<numeric_value_set>] <name> brightness [to] <brightness>',
|
||||
'sentence_template': '[<numeric_value_set>] [<the>] {name} brightness [to] {brightness}[([ ]%)| percent]',
|
||||
'slots': dict({
|
||||
'name': 'test light',
|
||||
}),
|
||||
'source': 'builtin',
|
||||
'targets': dict({
|
||||
}),
|
||||
'unmatched_slots': dict({
|
||||
|
||||
@@ -729,19 +729,6 @@ async def test_satellite_area_context(
|
||||
}
|
||||
turn_off_calls.clear()
|
||||
|
||||
# Turn on/off all lights also works
|
||||
for command in ("on", "off"):
|
||||
result = await conversation.async_converse(
|
||||
hass, f"turn {command} all lights", None, Context(), None
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
# All lights should have been targeted
|
||||
assert {s.entity_id for s in result.response.matched_states} == {
|
||||
e.entity_id for e in all_lights
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
async def test_error_no_device(hass: HomeAssistant) -> None:
|
||||
@@ -841,7 +828,7 @@ async def test_error_no_device_on_floor(
|
||||
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Sorry, I am not aware of any device called missing entity on ground floor"
|
||||
== "Sorry, I am not aware of any device called missing entity in the ground floor"
|
||||
)
|
||||
|
||||
|
||||
@@ -1128,7 +1115,7 @@ async def test_error_no_domain_on_floor_exposed(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "turn on all lights on the ground floor", None, Context(), None
|
||||
hass, "turn on all lights in the ground floor", None, Context(), None
|
||||
)
|
||||
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
@@ -1493,21 +1480,6 @@ async def test_error_duplicate_names_same_area(
|
||||
f" {name} in the {area_kitchen.name} area"
|
||||
)
|
||||
|
||||
# question
|
||||
result = await conversation.async_converse(
|
||||
hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None
|
||||
)
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert (
|
||||
result.response.error_code
|
||||
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||
)
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== f"Sorry, there are multiple devices called"
|
||||
f" {name} in the {area_kitchen.name} area"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
async def test_duplicate_names_same_area_but_one_is_exposed(
|
||||
@@ -2855,9 +2827,9 @@ async def test_config_sentences_priority(
|
||||
{
|
||||
"conversation": {
|
||||
"intents": {
|
||||
"CustomIntent": ["turn on <name>"],
|
||||
"CustomIntent": ["turn on [the] {name}"],
|
||||
"WorseCustomIntent": ["turn on the lamp"],
|
||||
"FakeCustomIntent": ["turn on <name>"],
|
||||
"FakeCustomIntent": ["turn on [the] {name}"],
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,29 +127,39 @@ async def test_cover_set_position(
|
||||
async def test_cover_device_class(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
area_registry: ar.AreaRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the open position for covers by device class."""
|
||||
await cover_intent.async_setup_intents(hass)
|
||||
|
||||
entity_id = f"{cover.DOMAIN}.front"
|
||||
hass.states.async_set(
|
||||
entity_id, STATE_CLOSED, attributes={"device_class": "garage"}
|
||||
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||
|
||||
kitchen_window = entity_registry.async_get_or_create(
|
||||
"cover", "demo", "kitchen_window"
|
||||
)
|
||||
async_expose_entity(hass, conversation.DOMAIN, entity_id, True)
|
||||
kitchen_window = entity_registry.async_update_entity(
|
||||
kitchen_window.entity_id, area_id=area_kitchen.id
|
||||
)
|
||||
hass.states.async_set(
|
||||
kitchen_window.entity_id, STATE_CLOSED, attributes={"device_class": "window"}
|
||||
)
|
||||
async_expose_entity(hass, conversation.DOMAIN, kitchen_window.entity_id, True)
|
||||
|
||||
# Open service
|
||||
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER)
|
||||
result = await conversation.async_converse(
|
||||
hass, "open the garage door", None, Context(), None
|
||||
hass, "open the window in the kitchen", None, Context(), None, device_id=None
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = result.response
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.speech["plain"]["speech"] == "Opening the garage"
|
||||
assert response.speech["plain"]["speech"] == "Opening the window"
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call.data == {"entity_id": entity_id}
|
||||
assert call.data == {"entity_id": kitchen_window.entity_id}
|
||||
|
||||
|
||||
async def test_valve_intents(
|
||||
|
||||
@@ -128,6 +128,54 @@ async def test_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
("token", "expected_step"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
},
|
||||
"reauth_confirm",
|
||||
),
|
||||
(
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": "account_info.read files.content.read files.content.write",
|
||||
},
|
||||
"reauth_permissions",
|
||||
),
|
||||
],
|
||||
ids=["missing_refresh_token", "missing_scope"],
|
||||
)
|
||||
async def test_reauth_confirm_step(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry,
|
||||
token: dict[str, object],
|
||||
expected_step: str,
|
||||
) -> None:
|
||||
"""Test reauth shows the correct confirmation step for the broken token."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry, data={**mock_config_entry.data, "token": token}
|
||||
)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == expected_step
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["step_id"] == "auth"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Test the Dropbox integration setup."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from python_dropbox_api import DropboxAuthException, DropboxUnknownException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.components.dropbox.const import DOMAIN, OAUTH2_SCOPES
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -80,6 +82,46 @@ async def test_setup_entry_implementation_unavailable(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_dropbox_client")
|
||||
@pytest.mark.parametrize(
|
||||
"token",
|
||||
[
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
},
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": "account_info.read files.content.read files.content.write",
|
||||
},
|
||||
],
|
||||
ids=["missing_refresh_token", "missing_scope"],
|
||||
)
|
||||
async def test_setup_entry_triggers_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
token: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that a broken token triggers a reauth flow during setup."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry, data={**mock_config_entry.data, "token": token}
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_dropbox_client")
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -13,6 +13,7 @@ from duco_connectivity import (
|
||||
Node,
|
||||
NodeActionItemList,
|
||||
NodeListActionItemList,
|
||||
NodeType,
|
||||
VentilationState,
|
||||
)
|
||||
import pytest
|
||||
@@ -32,6 +33,7 @@ from . import setup_platform_integration
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_SELECT_ENTITY = "select.living_ventilation_state"
|
||||
_VALVE_SELECT_ENTITY = "select.bedroom_valve_ventilation_state"
|
||||
_UNSUPPORTED_SELECT_ENTITY = "select.office_co2_ventilation_state"
|
||||
|
||||
|
||||
@@ -120,20 +122,55 @@ async def test_select_entity_created_with_dynamic_options(
|
||||
assert hass.states.get(_UNSUPPORTED_SELECT_ENTITY) is None
|
||||
|
||||
|
||||
async def test_select_ignores_non_box_nodes_even_when_actions_exist(
|
||||
@pytest.mark.parametrize(
|
||||
"valve_node_type",
|
||||
[
|
||||
pytest.param(NodeType.VLV, id="vlv"),
|
||||
pytest.param(NodeType.VLVRH, id="vlvrh"),
|
||||
pytest.param(NodeType.VLVVOC, id="vlvvoc"),
|
||||
pytest.param(NodeType.VLVCO2, id="vlvco2"),
|
||||
pytest.param(NodeType.VLVCO2RH, id="vlvco2rh"),
|
||||
pytest.param(NodeType.EAV, id="eav"),
|
||||
pytest.param(NodeType.EAVRH, id="eavrh"),
|
||||
pytest.param(NodeType.EAVVOC, id="eavvoc"),
|
||||
pytest.param(NodeType.EAVCO2, id="eavco2"),
|
||||
],
|
||||
)
|
||||
async def test_select_creates_entities_for_controllable_valve_nodes(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
valve_node_type: NodeType,
|
||||
) -> None:
|
||||
"""Test select discovery ignores non-box nodes that expose the same action."""
|
||||
"""Test select discovery includes valve nodes when they advertise control."""
|
||||
mock_nodes = [
|
||||
replace(
|
||||
mock_sensor_nodes[0],
|
||||
general=replace(mock_sensor_nodes[0].general, node_type=valve_node_type),
|
||||
),
|
||||
*mock_sensor_nodes[1:],
|
||||
]
|
||||
mock_duco_client.async_get_nodes.return_value = mock_nodes
|
||||
mock_duco_client.async_get_node_actions.return_value = _build_multi_node_actions(
|
||||
[1, 2, 50, 113],
|
||||
[node.node_id for node in mock_nodes],
|
||||
options=["AUTO", "CNT1", "CNT2", "CNT3", "MAN1", "MAN2", "MAN3"],
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.SELECT])
|
||||
|
||||
assert hass.states.get(_SELECT_ENTITY) is not None
|
||||
valve_state = hass.states.get(_VALVE_SELECT_ENTITY)
|
||||
assert valve_state is not None
|
||||
assert valve_state.attributes[ATTR_OPTIONS] == [
|
||||
"AUTO",
|
||||
"CNT1",
|
||||
"CNT2",
|
||||
"CNT3",
|
||||
"MAN1",
|
||||
"MAN2",
|
||||
"MAN3",
|
||||
]
|
||||
assert hass.states.get(_UNSUPPORTED_SELECT_ENTITY) is None
|
||||
|
||||
|
||||
|
||||
@@ -853,12 +853,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -1768,12 +1767,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -1822,8 +1820,14 @@
|
||||
]),
|
||||
}),
|
||||
'fixtures': dict({
|
||||
'/admin/lib/acb_config': 'Testing request replies.',
|
||||
'/admin/lib/acb_config_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/network_display': 'Testing request replies.',
|
||||
'/admin/lib/network_display_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/tariff': 'Testing request replies.',
|
||||
'/admin/lib/tariff_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/wireless_display': 'Testing request replies.',
|
||||
'/admin/lib/wireless_display_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/api/v1/production': 'Testing request replies.',
|
||||
'/api/v1/production/inverters': 'Testing request replies.',
|
||||
'/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
@@ -1832,6 +1836,8 @@
|
||||
'/home_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/info': 'Testing request replies.',
|
||||
'/info_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/inventory.json?deleted=1': 'Testing request replies.',
|
||||
'/inventory.json?deleted=1_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/dry_contacts': 'Testing request replies.',
|
||||
'/ivp/ensemble/dry_contacts_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/generator': 'Testing request replies.',
|
||||
@@ -1840,18 +1846,26 @@
|
||||
'/ivp/ensemble/inventory_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/power': 'Testing request replies.',
|
||||
'/ivp/ensemble/power_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/relay': 'Testing request replies.',
|
||||
'/ivp/ensemble/relay_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/secctrl': 'Testing request replies.',
|
||||
'/ivp/ensemble/secctrl_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/status': 'Testing request replies.',
|
||||
'/ivp/ensemble/status_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/livedata/status': 'Testing request replies.',
|
||||
'/ivp/livedata/status_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/meters': 'Testing request replies.',
|
||||
'/ivp/meters/readings': 'Testing request replies.',
|
||||
'/ivp/meters/readings_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/meters_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/pdm/device_data': 'Testing request replies.',
|
||||
'/ivp/pdm/device_data_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/pdm/energy': 'Testing request replies.',
|
||||
'/ivp/pdm/energy_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/sc/pvlimit': 'Testing request replies.',
|
||||
'/ivp/sc/pvlimit_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/sc/sched': 'Testing request replies.',
|
||||
'/ivp/sc/sched_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ss/dry_contact_settings': 'Testing request replies.',
|
||||
'/ivp/ss/dry_contact_settings_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ss/gen_config': 'Testing request replies.',
|
||||
@@ -2727,12 +2741,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -2781,9 +2794,18 @@
|
||||
]),
|
||||
}),
|
||||
'fixtures': dict({
|
||||
'/admin/lib/acb_config_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/network_display_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/tariff_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/wireless_display_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/api/v1/production/inverters_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2796,6 +2818,9 @@
|
||||
'/info_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/inventory.json?deleted=1_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/dry_contacts_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2808,12 +2833,18 @@
|
||||
'/ivp/ensemble/power_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/relay_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/secctrl_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/status_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/livedata/status_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/meters/readings_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2823,9 +2854,15 @@
|
||||
'/ivp/pdm/device_data_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/pdm/energy_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/sc/pvlimit_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/sc/sched_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ss/dry_contact_settings_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -3711,12 +3748,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -19745,59 +19781,16 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': dict({
|
||||
'__type': "<class 'pyenphase.models.c6combiner.EnvoyC6CC'>",
|
||||
'repr': "EnvoyC6CC(admin_state=82, admin_state_str='ENCMN_C6_CC_READY', firmware_loaded_date=1752945451, firmware_version='0.1.20-D1', installed_date=1752945451, last_report_date=1752945451, communicating=True, part_number='800-02403-r08', serial_number='482523040549', dmir_version='0.1.20-D1')",
|
||||
}),
|
||||
'ctmeter_consumption_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
}),
|
||||
'ctmeter_production': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance', 'power-on-unused-phase'])",
|
||||
}),
|
||||
'ctmeter_production_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance'])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['power-on-unused-phase'])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
}),
|
||||
'ctmeter_storage': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'ctmeter_storage_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'collar': dict({
|
||||
'__type': "<class 'pyenphase.models.collar.EnvoyCollar'>",
|
||||
'repr': "EnvoyCollar(admin_state=88, admin_state_str='ENCMN_MDE_ON_GRID', firmware_loaded_date=1752939759, firmware_version='3.0.6-D0', installed_date=1752939759, last_report_date=1752939759, communicating=True, mid_state='close', grid_state='on_grid', part_number='865-00400-r22', serial_number='482520020939', temperature=42, temperature_unit='C', control_error=0, collar_state='Installed')",
|
||||
}),
|
||||
'ctmeters': dict({
|
||||
'backfeed': dict({
|
||||
|
||||
@@ -107,12 +107,12 @@ async def test_job_manager_ws_updates(
|
||||
job_data: Job | None = None
|
||||
|
||||
@callback
|
||||
def mock_subcription_callback(job: Job) -> None:
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
nonlocal job_data
|
||||
job_data = job
|
||||
|
||||
subscription = JobSubscription(
|
||||
mock_subcription_callback, name="test_job", reference="test"
|
||||
mock_subscription_callback, name="test_job", reference="test"
|
||||
)
|
||||
unsubscribe = data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
@@ -318,11 +318,11 @@ async def test_job_manager_reload_on_supervisor_restart(
|
||||
job_data: Job | None = None
|
||||
|
||||
@callback
|
||||
def mock_subcription_callback(job: Job) -> None:
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
nonlocal job_data
|
||||
job_data = job
|
||||
|
||||
subscription = JobSubscription(mock_subcription_callback, name="test_job")
|
||||
subscription = JobSubscription(mock_subscription_callback, name="test_job")
|
||||
data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
# Send supervisor restart signal
|
||||
@@ -347,3 +347,78 @@ async def test_job_manager_reload_on_supervisor_restart(
|
||||
assert job_data.reference == "test"
|
||||
assert job_data.done is True
|
||||
assert not data_coordinator.jobs.current_jobs
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_subscribe_returns_unsubscribe_when_job_already_matches(
|
||||
hass: HomeAssistant,
|
||||
jobs_info: AsyncMock,
|
||||
hass_supervisor_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test subscribe returns a working unsubscribe even if a job already matches."""
|
||||
jobs_info.return_value = JobsInfo(
|
||||
ignore_conditions=[],
|
||||
jobs=[
|
||||
Job(
|
||||
name="test_job",
|
||||
reference="test",
|
||||
uuid=uuid4(),
|
||||
progress=0,
|
||||
stage=None,
|
||||
done=False,
|
||||
errors=[],
|
||||
created=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
extra=None,
|
||||
child_jobs=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, DOMAIN, {})
|
||||
assert result
|
||||
|
||||
client = await hass_supervisor_ws_client()
|
||||
data_coordinator: HassioMainDataUpdateCoordinator = hass.data[MAIN_COORDINATOR]
|
||||
|
||||
received: list[Job] = []
|
||||
|
||||
@callback
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
received.append(job)
|
||||
|
||||
subscription = JobSubscription(mock_subscription_callback, name="test_job")
|
||||
unsubscribe = data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
# Existing matching job is delivered immediately, and a callable unsubscribe
|
||||
# is returned (not the None result of the callback)
|
||||
assert len(received) == 1
|
||||
assert received[0].name == "test_job"
|
||||
assert callable(unsubscribe)
|
||||
|
||||
# After unsubscribing, a new matching job update is no longer delivered
|
||||
unsubscribe()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "job",
|
||||
"data": {
|
||||
"name": "test_job",
|
||||
"reference": "test",
|
||||
"uuid": uuid4().hex,
|
||||
"progress": 50,
|
||||
"stage": None,
|
||||
"done": False,
|
||||
"errors": [],
|
||||
"created": datetime.now().isoformat(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
"extra": None,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(received) == 1
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for the logentries component."""
|
||||
@@ -1,75 +0,0 @@
|
||||
"""The tests for the Logentries component."""
|
||||
|
||||
from unittest.mock import ANY, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import logentries
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_setup_config_full(hass: HomeAssistant) -> None:
|
||||
"""Test setup with all data."""
|
||||
config = {"logentries": {"token": "secret"}}
|
||||
assert await async_setup_component(hass, logentries.DOMAIN, config)
|
||||
|
||||
with patch("homeassistant.components.logentries.requests.post") as mock_post:
|
||||
hass.states.async_set("fake.entity", STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_post.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_setup_config_defaults(hass: HomeAssistant) -> None:
|
||||
"""Test setup with defaults."""
|
||||
config = {"logentries": {"token": "token"}}
|
||||
assert await async_setup_component(hass, logentries.DOMAIN, config)
|
||||
|
||||
with patch("homeassistant.components.logentries.requests.post") as mock_post:
|
||||
hass.states.async_set("fake.entity", STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_post.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dump():
|
||||
"""Mock json dumps."""
|
||||
with patch("json.dumps") as mock_dump:
|
||||
yield mock_dump
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_requests():
|
||||
"""Mock requests."""
|
||||
with patch.object(logentries, "requests") as mock_requests:
|
||||
yield mock_requests
|
||||
|
||||
|
||||
async def test_event_listener(hass: HomeAssistant, mock_dump, mock_requests) -> None:
|
||||
"""Test event listener."""
|
||||
mock_dump.side_effect = lambda x: x
|
||||
mock_post = mock_requests.post
|
||||
mock_requests.exceptions.RequestException = Exception
|
||||
config = {"logentries": {"token": "token"}}
|
||||
assert await async_setup_component(hass, logentries.DOMAIN, config)
|
||||
|
||||
valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0, "foo": "foo"}
|
||||
for in_, out in valid.items():
|
||||
payload = {
|
||||
"host": "https://webhook.logentries.com/noformat/logs/token",
|
||||
"event": [
|
||||
{
|
||||
"domain": "fake",
|
||||
"entity_id": "entity",
|
||||
"attributes": {},
|
||||
"time": ANY,
|
||||
"value": out,
|
||||
}
|
||||
],
|
||||
}
|
||||
hass.states.async_set("fake.entity", in_)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_post.call_count == 1
|
||||
assert mock_post.call_args == call(payload["host"], data=payload, timeout=10)
|
||||
mock_post.reset_mock()
|
||||
@@ -4765,3 +4765,46 @@ async def test_import_statistics_with_last_reset(
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def test_entity_options_ws(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test recorder entity options WS commands."""
|
||||
client = await hass_ws_client()
|
||||
|
||||
await async_setup_recorder_instance(hass, {"exclude": {"domains": "test2"}})
|
||||
|
||||
# Test getting a single entity's settings
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/entity_options/get",
|
||||
"entity_id": "test.recorder",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"recording_disabled_by": None}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/entity_options/get",
|
||||
"entity_id": "test2.recorder",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"recording_disabled_by": "user"}
|
||||
|
||||
# Test getting settings for an unknown entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/entity_options/get",
|
||||
"entity_id": "unknown.entity",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"recording_disabled_by": None}
|
||||
|
||||
@@ -318,6 +318,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -359,6 +360,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -1143,6 +1145,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -1184,6 +1187,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -2411,6 +2415,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -2452,6 +2457,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -3400,6 +3406,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -3441,6 +3448,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -4273,6 +4281,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -4314,6 +4323,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -5088,6 +5098,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -5129,6 +5140,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -5971,6 +5983,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
@@ -6012,6 +6025,7 @@
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'always',
|
||||
'delayed',
|
||||
'delegated',
|
||||
'scheduled',
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -1076,7 +1076,7 @@ async def test_privacy_mode_change_callback(
|
||||
def register_callback(
|
||||
self, callback_id: str, callback: Callable[[], None], *args, **key_args
|
||||
) -> None:
|
||||
if callback_id == "privacy_mode_change":
|
||||
if callback_id == "privacy_mode_change_623":
|
||||
self.callback_func = callback
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
from collections.abc import Callable, Generator
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
import pathlib
|
||||
import tempfile
|
||||
@@ -272,12 +273,9 @@ def make_mock_switch(
|
||||
return trait
|
||||
|
||||
|
||||
def make_dnd_timer(dataclass_template: RoborockBase) -> AsyncMock:
|
||||
"""Make a function for the fake timer trait that emulates the real behavior."""
|
||||
dnd_trait = make_mock_switch(
|
||||
trait_spec=DoNotDisturbTrait,
|
||||
dataclass_template=dataclass_template,
|
||||
)
|
||||
def make_dnd_timer(dataclass_template: RoborockBase) -> DoNotDisturbTrait:
|
||||
"""Create a DoNotDisturbTrait for testing."""
|
||||
dnd_trait = DoNotDisturbTrait(**asdict(dataclass_template))
|
||||
|
||||
async def set_dnd_timer(timer: DnDTimer) -> None:
|
||||
dnd_trait.start_hour = timer.start_hour
|
||||
@@ -286,16 +284,19 @@ def make_dnd_timer(dataclass_template: RoborockBase) -> AsyncMock:
|
||||
dnd_trait.end_minute = timer.end_minute
|
||||
dnd_trait.enabled = timer.enabled
|
||||
|
||||
dnd_trait.set_dnd_timer = AsyncMock()
|
||||
dnd_trait.set_dnd_timer.side_effect = set_dnd_timer
|
||||
dnd_trait.set_dnd_timer = AsyncMock(side_effect=set_dnd_timer)
|
||||
dnd_trait.enable = AsyncMock(side_effect=lambda: setattr(dnd_trait, "enabled", 1))
|
||||
dnd_trait.disable = AsyncMock(side_effect=lambda: setattr(dnd_trait, "enabled", 0))
|
||||
dnd_trait.refresh = AsyncMock()
|
||||
return dnd_trait
|
||||
|
||||
|
||||
def make_valley_electric_timer(dataclass_template: RoborockBase) -> AsyncMock:
|
||||
"""Make a function for the fake timer trait that emulates the real behavior."""
|
||||
valley_electric_timer_trait = make_mock_switch(
|
||||
trait_spec=ValleyElectricityTimerTrait,
|
||||
dataclass_template=dataclass_template,
|
||||
def make_valley_electric_timer(
|
||||
dataclass_template: RoborockBase,
|
||||
) -> ValleyElectricityTimerTrait:
|
||||
"""Create a ValleyElectricityTimerTrait for testing."""
|
||||
valley_electric_timer_trait = ValleyElectricityTimerTrait(
|
||||
**asdict(dataclass_template)
|
||||
)
|
||||
|
||||
async def set_timer(timer: ValleyElectricityTimer) -> None:
|
||||
@@ -305,8 +306,14 @@ def make_valley_electric_timer(dataclass_template: RoborockBase) -> AsyncMock:
|
||||
valley_electric_timer_trait.end_minute = timer.end_minute
|
||||
valley_electric_timer_trait.enabled = timer.enabled
|
||||
|
||||
valley_electric_timer_trait.set_timer = AsyncMock()
|
||||
valley_electric_timer_trait.set_timer.side_effect = set_timer
|
||||
valley_electric_timer_trait.set_timer = AsyncMock(side_effect=set_timer)
|
||||
valley_electric_timer_trait.enable = AsyncMock(
|
||||
side_effect=lambda: setattr(valley_electric_timer_trait, "enabled", 1)
|
||||
)
|
||||
valley_electric_timer_trait.disable = AsyncMock(
|
||||
side_effect=lambda: setattr(valley_electric_timer_trait, "enabled", 0)
|
||||
)
|
||||
valley_electric_timer_trait.refresh = AsyncMock()
|
||||
return valley_electric_timer_trait
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import pytest
|
||||
from roborock.exceptions import RoborockTimeout
|
||||
|
||||
from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
|
||||
from .conftest import FakeDevice
|
||||
|
||||
@@ -49,6 +50,22 @@ async def test_update_sound_volume(
|
||||
assert state.state == "3.0"
|
||||
|
||||
|
||||
async def test_volume_unknown_value(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test the entity reports unknown when the trait value is None."""
|
||||
assert fake_vacuum.v1_properties is not None
|
||||
fake_vacuum.v1_properties.sound_volume.volume = None
|
||||
|
||||
await async_update_entity(hass, "number.roborock_s7_maxv_volume")
|
||||
|
||||
state = hass.states.get("number.roborock_s7_maxv_volume")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_volume_update_failed(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
"""Test Roborock Time platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import time
|
||||
from datetime import time, timedelta
|
||||
from typing import Any
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
import roborock
|
||||
from roborock.data import DnDTimer, RoborockBaseTimer, ValleyElectricityTimer
|
||||
|
||||
from homeassistant.components.time import SERVICE_SET_VALUE
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import FakeDevice
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -127,3 +128,53 @@ async def test_update_failure(
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
assert fake_vacuum.v1_properties.dnd.set_dnd_timer.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "trait", "missing_attribute"),
|
||||
[
|
||||
(
|
||||
"time.roborock_s7_maxv_do_not_disturb_begin",
|
||||
lambda x: x.v1_properties.dnd,
|
||||
"start_hour",
|
||||
),
|
||||
(
|
||||
"time.roborock_s7_maxv_do_not_disturb_begin",
|
||||
lambda x: x.v1_properties.dnd,
|
||||
"start_minute",
|
||||
),
|
||||
(
|
||||
"time.roborock_s7_maxv_do_not_disturb_end",
|
||||
lambda x: x.v1_properties.dnd,
|
||||
"end_hour",
|
||||
),
|
||||
(
|
||||
"time.roborock_s7_maxv_off_peak_start",
|
||||
lambda x: x.v1_properties.valley_electricity_timer,
|
||||
"start_minute",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_missing_value(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_vacuum: FakeDevice,
|
||||
entity_id: str,
|
||||
trait: Callable[[FakeDevice], Any],
|
||||
missing_attribute: str,
|
||||
) -> None:
|
||||
"""Test that a missing time component reports as unknown instead of raising."""
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNKNOWN
|
||||
|
||||
setattr(trait(fake_vacuum), missing_attribute, None)
|
||||
freezer.tick(timedelta(seconds=31))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
@@ -16,8 +16,11 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
# San Diego (default test location) and Longyearbyen, Svalbard (deep polar).
|
||||
# San Diego (default test location), Kotzebue, Alaska (just inside the Arctic
|
||||
# Circle - brief midnight sun in June) and Longyearbyen, Svalbard (deep polar -
|
||||
# long polar night in December).
|
||||
_SAN_DIEGO = (32.87336, -117.22743, "US/Pacific")
|
||||
_KOTZEBUE = (66.8983, -162.5966, "America/Anchorage")
|
||||
_SVALBARD = (78.22, 15.65, "Europe/Oslo")
|
||||
|
||||
_TWILIGHT_TYPES = ("any", "civil", "nautical", "astronomical")
|
||||
@@ -38,8 +41,8 @@ def _find_run_id(traces, trace_type, item_id):
|
||||
return None
|
||||
|
||||
|
||||
async def assert_automation_condition_trace(hass_ws_client, automation_id, expected):
|
||||
"""Test the result of automation condition."""
|
||||
async def _get_automation_condition_trace(hass_ws_client, automation_id):
|
||||
"""Return the condition trace for a given automation."""
|
||||
msg_id = 1
|
||||
|
||||
def next_id():
|
||||
@@ -71,8 +74,15 @@ async def assert_automation_condition_trace(hass_ws_client, automation_id, expec
|
||||
assert response["success"]
|
||||
trace = response["result"]
|
||||
assert len(trace["trace"]["condition/0"]) == 1
|
||||
condition_trace = trace["trace"]["condition/0"][0]["result"]
|
||||
assert condition_trace == expected
|
||||
return trace["trace"]["condition/0"][0]
|
||||
|
||||
|
||||
async def assert_automation_condition_trace(hass_ws_client, automation_id, expected):
|
||||
"""Test the result of automation condition."""
|
||||
condition_trace = await _get_automation_condition_trace(
|
||||
hass_ws_client, automation_id
|
||||
)
|
||||
assert condition_trace["result"] == expected
|
||||
|
||||
|
||||
async def test_if_action_before_sunrise_no_offset(
|
||||
@@ -937,14 +947,14 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
) -> None:
|
||||
"""Test if action was before sunrise.
|
||||
|
||||
Local timezone: Alaska time
|
||||
Location: Kotzebue, which has a very skewed local timezone with sunrise
|
||||
at 7 AM and sunset at 3AM during summer
|
||||
After sunrise is true from sunrise until midnight, local time.
|
||||
Local timezone: Alaska time (America/Anchorage)
|
||||
Location: Kotzebue, Alaska, whose far-west longitude skews local time by
|
||||
~3 hours, so in late July sunrise is ~04:48 local. Before sunrise is true
|
||||
from local midnight until sunrise.
|
||||
"""
|
||||
await hass.config.async_set_time_zone("America/Anchorage")
|
||||
hass.config.latitude = 66.5
|
||||
hass.config.longitude = 162.4
|
||||
hass.config.latitude = 66.8983
|
||||
hass.config.longitude = -162.5966
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
@@ -961,10 +971,9 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
},
|
||||
)
|
||||
|
||||
# sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
|
||||
# sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
|
||||
# sunrise: 2015-07-24 04:48:24 local = 2015-07-24 12:48:24 UTC
|
||||
# now = sunrise + 1s -> 'before sunrise' not true
|
||||
now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC)
|
||||
now = datetime(2015, 7, 24, 12, 48, 25, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -972,11 +981,11 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": False, "wanted_time_before": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = sunrise - 1h -> 'before sunrise' true
|
||||
now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC)
|
||||
now = datetime(2015, 7, 24, 11, 48, 24, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -984,7 +993,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": True, "wanted_time_before": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight -> 'before sunrise' true
|
||||
@@ -996,7 +1005,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": True, "wanted_time_before": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight - 1s -> 'before sunrise' not true
|
||||
@@ -1008,7 +1017,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_before": "2015-07-23T15:12:19.155123+00:00"},
|
||||
{"result": False, "wanted_time_before": "2015-07-23T12:43:32.413351+00:00"},
|
||||
)
|
||||
|
||||
|
||||
@@ -1019,14 +1028,14 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
) -> None:
|
||||
"""Test if action was after sunrise.
|
||||
|
||||
Local timezone: Alaska time
|
||||
Location: Kotzebue, which has a very skewed local timezone with sunrise
|
||||
at 7 AM and sunset at 3AM during summer
|
||||
Before sunrise is true from midnight until sunrise, local time.
|
||||
Local timezone: Alaska time (America/Anchorage)
|
||||
Location: Kotzebue, Alaska, whose far-west longitude skews local time by
|
||||
~3 hours, so in late July sunrise is ~04:48 local. After sunrise is true
|
||||
from sunrise until local midnight.
|
||||
"""
|
||||
await hass.config.async_set_time_zone("America/Anchorage")
|
||||
hass.config.latitude = 66.5
|
||||
hass.config.longitude = 162.4
|
||||
hass.config.latitude = 66.8983
|
||||
hass.config.longitude = -162.5966
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
@@ -1043,10 +1052,9 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
},
|
||||
)
|
||||
|
||||
# sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
|
||||
# sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
|
||||
# now = sunrise -> 'after sunrise' true
|
||||
now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC)
|
||||
# sunrise: 2015-07-24 04:48:24 local = 2015-07-24 12:48:24 UTC
|
||||
# now = sunrise + 1s -> 'after sunrise' true
|
||||
now = datetime(2015, 7, 24, 12, 48, 25, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1054,11 +1062,11 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": True, "wanted_time_after": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = sunrise - 1h -> 'after sunrise' not true
|
||||
now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC)
|
||||
now = datetime(2015, 7, 24, 11, 48, 24, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1066,7 +1074,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": False, "wanted_time_after": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight -> 'after sunrise' not true
|
||||
@@ -1078,7 +1086,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"},
|
||||
{"result": False, "wanted_time_after": "2015-07-24T12:48:24.249497+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight - 1s -> 'after sunrise' true
|
||||
@@ -1090,7 +1098,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_after": "2015-07-23T15:12:19.155123+00:00"},
|
||||
{"result": True, "wanted_time_after": "2015-07-23T12:43:32.413351+00:00"},
|
||||
)
|
||||
|
||||
|
||||
@@ -1099,16 +1107,17 @@ async def test_if_action_before_sunset_no_offset_kotzebue(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test if action was before sunrise.
|
||||
"""Test if action was before sunset on a day with two sunsets.
|
||||
|
||||
Local timezone: Alaska time
|
||||
Location: Kotzebue, which has a very skewed local timezone with sunrise
|
||||
at 7 AM and sunset at 3AM during summer
|
||||
Before sunset is true from midnight until sunset, local time.
|
||||
Local timezone: Alaska time (America/Anchorage)
|
||||
Location: Kotzebue, Alaska. On 2015-08-07 (local) the sun sets twice - at
|
||||
00:03 and again at 23:59 - because solar midnight falls near local midnight.
|
||||
The condition tracks the day's (late) sunset, so 'before sunset' stays true
|
||||
across the early sunset and only turns false after the late one.
|
||||
"""
|
||||
await hass.config.async_set_time_zone("America/Anchorage")
|
||||
hass.config.latitude = 66.5
|
||||
hass.config.longitude = 162.4
|
||||
hass.config.latitude = 66.8983
|
||||
hass.config.longitude = -162.5966
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
@@ -1125,22 +1134,9 @@ async def test_if_action_before_sunset_no_offset_kotzebue(
|
||||
},
|
||||
)
|
||||
|
||||
# sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
|
||||
# sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
|
||||
# now = sunset + 1s -> 'before sunset' not true
|
||||
now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"},
|
||||
)
|
||||
|
||||
# now = sunset - 1h-> 'before sunset' true
|
||||
now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC)
|
||||
# 2015-08-07 local has two sunsets: 00:03 (08:03 UTC) and 23:59 (08-08 07:59 UTC)
|
||||
# now = local midnight -> 'before sunset' true
|
||||
now = datetime(2015, 8, 7, 8, 0, 0, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1148,11 +1144,11 @@ async def test_if_action_before_sunset_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"},
|
||||
{"result": True, "wanted_time_before": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight -> 'before sunrise' true
|
||||
now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC)
|
||||
# now = first (early) sunset + 1s -> still 'before sunset' (tracks the late one)
|
||||
now = datetime(2015, 8, 7, 8, 3, 43, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1160,19 +1156,31 @@ async def test_if_action_before_sunset_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_before": "2015-07-24T11:17:54.446913+00:00"},
|
||||
{"result": True, "wanted_time_before": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight - 1s -> 'before sunrise' not true
|
||||
now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC)
|
||||
# now = late sunset - 1h -> 'before sunset' true
|
||||
now = datetime(2015, 8, 8, 6, 59, 25, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
assert len(service_calls) == 3
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_before": "2015-07-23T11:22:18.467277+00:00"},
|
||||
{"result": True, "wanted_time_before": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = late sunset + 1s -> 'before sunset' not true
|
||||
now = datetime(2015, 8, 8, 7, 59, 26, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 3
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_before": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
|
||||
@@ -1181,16 +1189,17 @@ async def test_if_action_after_sunset_no_offset_kotzebue(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test if action was after sunrise.
|
||||
"""Test if action was after sunset on a day with two sunsets.
|
||||
|
||||
Local timezone: Alaska time
|
||||
Location: Kotzebue, which has a very skewed local timezone with sunrise
|
||||
at 7 AM and sunset at 3AM during summer
|
||||
After sunset is true from sunset until midnight, local time.
|
||||
Local timezone: Alaska time (America/Anchorage)
|
||||
Location: Kotzebue, Alaska. On 2015-08-07 (local) the sun sets twice - at
|
||||
00:03 and again at 23:59. The condition tracks the day's (late) sunset, so
|
||||
'after sunset' is false right after the early sunset and only true in the
|
||||
short window after the late sunset before local midnight.
|
||||
"""
|
||||
await hass.config.async_set_time_zone("America/Anchorage")
|
||||
hass.config.latitude = 66.5
|
||||
hass.config.longitude = 162.4
|
||||
hass.config.latitude = 66.8983
|
||||
hass.config.longitude = -162.5966
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
@@ -1207,10 +1216,33 @@ async def test_if_action_after_sunset_no_offset_kotzebue(
|
||||
},
|
||||
)
|
||||
|
||||
# sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
|
||||
# sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
|
||||
# now = sunset -> 'after sunset' true
|
||||
now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC)
|
||||
# 2015-08-07 local has two sunsets: 00:03 (08:03 UTC) and 23:59 (08-08 07:59 UTC)
|
||||
# now = first (early) sunset + 1s -> 'after sunset' not true (tracks the late one)
|
||||
now = datetime(2015, 8, 7, 8, 4, 0, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = late sunset - 1s -> 'after sunset' not true
|
||||
now = datetime(2015, 8, 8, 7, 59, 25, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = late sunset + 1s -> 'after sunset' true
|
||||
now = datetime(2015, 8, 8, 7, 59, 27, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1218,11 +1250,11 @@ async def test_if_action_after_sunset_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"},
|
||||
{"result": True, "wanted_time_after": "2015-08-08T07:59:25.982224+00:00"},
|
||||
)
|
||||
|
||||
# now = sunset - 1s -> 'after sunset' not true
|
||||
now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC)
|
||||
# now = local midnight (next day) -> 'after sunset' not true
|
||||
now = datetime(2015, 8, 8, 8, 0, 1, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
@@ -1230,31 +1262,65 @@ async def test_if_action_after_sunset_no_offset_kotzebue(
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"},
|
||||
{"result": False, "wanted_time_after": "2015-08-09T07:55:10.646523+00:00"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("location", "now", "event"),
|
||||
[
|
||||
# Midnight sun at Kotzebue (early June to early July): the sun neither
|
||||
# rises nor sets, so neither a sunrise nor a sunset condition can be met.
|
||||
(_KOTZEBUE, datetime(2015, 6, 15, 12, tzinfo=dt_util.UTC), SUN_EVENT_SUNSET),
|
||||
(_KOTZEBUE, datetime(2015, 6, 15, 12, tzinfo=dt_util.UTC), SUN_EVENT_SUNRISE),
|
||||
# Polar night at Svalbard: the sun neither rises nor sets here either.
|
||||
(_SVALBARD, datetime(2015, 12, 15, 12, tzinfo=dt_util.UTC), SUN_EVENT_SUNSET),
|
||||
(_SVALBARD, datetime(2015, 12, 15, 12, tzinfo=dt_util.UTC), SUN_EVENT_SUNRISE),
|
||||
],
|
||||
)
|
||||
async def test_if_action_no_sun_event_in_polar_regions(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
service_calls: list[ServiceCall],
|
||||
location: tuple[float, float, str],
|
||||
now: datetime,
|
||||
event: str,
|
||||
) -> None:
|
||||
"""Test a sun condition where the requested event never occurs.
|
||||
|
||||
During midnight sun and polar night the sun neither rises nor sets, so
|
||||
``get_astral_event_date`` returns None for the requested event. The
|
||||
condition cannot be satisfied and reports "no sunrise today" / "no sunset
|
||||
today" instead of raising.
|
||||
"""
|
||||
latitude, longitude, time_zone = location
|
||||
await hass.config.async_set_time_zone(time_zone)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "sun",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": "sun",
|
||||
"options": {"after": event},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# now = local midnight -> 'after sunset' not true
|
||||
now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert len(service_calls) == 0
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": False, "wanted_time_after": "2015-07-24T11:17:54.446913+00:00"},
|
||||
)
|
||||
|
||||
# now = local midnight - 1s -> 'after sunset' true
|
||||
now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
await assert_automation_condition_trace(
|
||||
hass_ws_client,
|
||||
"sun",
|
||||
{"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"},
|
||||
{"result": False, "message": f"no {event} today"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -346,6 +346,12 @@ async def test_dawn_defaults_to_civil(
|
||||
_SVALBARD = (78.22, 15.65, "Europe/Oslo")
|
||||
_KOTZEBUE = (66.8983, -162.5966, "America/Anchorage")
|
||||
|
||||
# A two-sunrise day is the mirror of Kotzebue's two-sunset day: it needs solar
|
||||
# noon (not midnight) near local midnight, which no real location has because
|
||||
# time zones keep solar noon near local noon. This synthetic location forces it
|
||||
# with a polar latitude on a deliberately ~12 h-offset time zone.
|
||||
_TWO_SUNRISE_LOCATION = (66.5, -32.5, "America/Anchorage")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("location", "now", "trigger_key", "astral_event", "options", "depression"),
|
||||
@@ -452,6 +458,41 @@ async def test_two_sunsets_on_one_day_at_kotzebue(
|
||||
assert len(service_calls) == 2
|
||||
|
||||
|
||||
async def test_two_sunrises_on_one_day(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test both sunrises fire on a calendar day that has two.
|
||||
|
||||
The mirror of the two-sunset case: a synthetic polar location on a
|
||||
deliberately offset time zone puts solar noon near local midnight, so
|
||||
2015-03-07 (local) has two sunrises ~24 h apart - one just after midnight
|
||||
and one just before - and the scheduler must fire for both.
|
||||
"""
|
||||
latitude, longitude, time_zone = _TWO_SUNRISE_LOCATION
|
||||
await hass.config.async_set_time_zone(time_zone)
|
||||
await hass.config.async_update(latitude=latitude, longitude=longitude, elevation=0)
|
||||
|
||||
# 2015-03-07 09:02:36 UTC (00:02 local) and 2015-03-08 08:58:43 UTC (23:58 local)
|
||||
now = datetime(2015, 3, 7, 9, tzinfo=dt_util.UTC)
|
||||
with freeze_time(now) as freezer:
|
||||
await _arm_automation(hass, {"platform": "sun.sunrise"}, {})
|
||||
first = get_astral_event_next(hass, "sunrise", now)
|
||||
second = get_astral_event_next(hass, "sunrise", first)
|
||||
# Two sunrises ~24 h apart that share one local calendar day.
|
||||
assert dt_util.as_local(first).date() == dt_util.as_local(second).date()
|
||||
assert timedelta(hours=23) < second - first < timedelta(hours=25)
|
||||
|
||||
freezer.move_to(first + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass, first + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
freezer.move_to(second + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass, second + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "astral_event", "now", "above_horizon"),
|
||||
[
|
||||
|
||||
@@ -12,7 +12,16 @@ from .consts import SERIAL
|
||||
|
||||
def mock_dsm_hardware(fan_speed: FanSpeed = FanSpeed.COOL) -> Mock:
|
||||
"""Mock SynologyDSM hardware information."""
|
||||
return AsyncMock(update=AsyncMock(), fan_speed=fan_speed)
|
||||
return AsyncMock(
|
||||
update=AsyncMock(),
|
||||
fan_speed=fan_speed,
|
||||
supported_fan_speeds=[
|
||||
FanSpeed.FULL,
|
||||
FanSpeed.COOL,
|
||||
FanSpeed.QUIET,
|
||||
FanSpeed.QUIET_STOP,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def mock_dsm_information(
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'low_power',
|
||||
'quiet',
|
||||
'cool',
|
||||
'full_speed',
|
||||
@@ -48,6 +49,7 @@
|
||||
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Synology',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'nas.meontheinternet.com Fan speed mode',
|
||||
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'low_power',
|
||||
'quiet',
|
||||
'cool',
|
||||
'full_speed',
|
||||
@@ -61,3 +63,63 @@
|
||||
'state': 'cool',
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_speed_supported_option[select.nas_meontheinternet_com_fan_speed_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'cool',
|
||||
'full_speed',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.nas_meontheinternet_com_fan_speed_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Fan speed mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fan speed mode',
|
||||
'platform': 'synology_dsm',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fan_speed_mode',
|
||||
'unique_id': 'mySerial_SYNO.Core.Hardware:fan_speed_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_speed_supported_option[select.nas_meontheinternet_com_fan_speed_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Synology',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'nas.meontheinternet.com Fan speed mode',
|
||||
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'cool',
|
||||
'full_speed',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.nas_meontheinternet_com_fan_speed_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'cool',
|
||||
})
|
||||
# ---
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user