mirror of
https://github.com/home-assistant/core.git
synced 2025-10-08 11:19:30 +00:00
Compare commits
24 Commits
water_hier
...
trigger_ac
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b0d69533eb | ||
![]() |
bfa2f8098e | ||
![]() |
0ebd210252 | ||
![]() |
6027b697c9 | ||
![]() |
7a86007083 | ||
![]() |
a6e21a54a3 | ||
![]() |
8b3cb69b79 | ||
![]() |
f296a215e7 | ||
![]() |
4b5fd38849 | ||
![]() |
308f6eb5a8 | ||
![]() |
82f1ae3519 | ||
![]() |
b8660b4248 | ||
![]() |
fba50af1c3 | ||
![]() |
bdd448fbe0 | ||
![]() |
c3f45d594b | ||
![]() |
a95af1a40e | ||
![]() |
fa863649fa | ||
![]() |
b7c6e21707 | ||
![]() |
e7da1250ba | ||
![]() |
e71140e09b | ||
![]() |
53875f7188 | ||
![]() |
526541f666 | ||
![]() |
01d81f8980 | ||
![]() |
7d96a814f9 |
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"cloud_ceiling": {
|
||||
"default": "mdi:weather-fog"
|
||||
},
|
||||
@@ -37,6 +34,9 @@
|
||||
"thunderstorm_probability_night": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"translation_key": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"tree_pollen": {
|
||||
"default": "mdi:tree-outline"
|
||||
},
|
||||
|
@@ -1,9 +1,7 @@
|
||||
"""Airgradient Update platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@@ -15,7 +13,6 @@ from .entity import AirGradientEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -34,7 +31,6 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Representation of Airgradient Update."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_server_unreachable_logged = False
|
||||
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
@@ -51,27 +47,10 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Return the installed version of the entity."""
|
||||
return self.coordinator.data.measures.firmware_version
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._attr_available
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
try:
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
)
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
)
|
||||
except AirGradientConnectionError:
|
||||
self._attr_latest_version = None
|
||||
self._attr_available = False
|
||||
if not self._server_unreachable_logged:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
)
|
||||
self._server_unreachable_logged = True
|
||||
else:
|
||||
self._server_unreachable_logged = False
|
||||
self._attr_available = True
|
||||
)
|
||||
|
@@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -30,19 +29,23 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoBinarySensorEntity(
|
||||
coordinator, device, config_entry.entry_id
|
||||
)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitVedoBinarySensorEntity(
|
||||
|
@@ -7,21 +7,14 @@ from typing import Any, cast
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -36,19 +29,21 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, COVER)
|
||||
)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[COVER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[COVER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@@ -67,6 +62,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
super().__init__(coordinator, device, config_entry_entry_id)
|
||||
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
||||
self._last_action: int | None = None
|
||||
self._last_state: str | None = None
|
||||
|
||||
def _current_action(self, action: str) -> bool:
|
||||
"""Return the current cover action."""
|
||||
@@ -102,6 +98,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@bridge_api_call
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
@@ -127,10 +124,5 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
if state.state == STATE_CLOSED:
|
||||
self._last_action = STATE_COVER.index(STATE_CLOSING)
|
||||
if state.state == STATE_OPEN:
|
||||
self._last_action = STATE_COVER.index(STATE_OPENING)
|
||||
|
||||
self._attr_is_closed = state.state == STATE_CLOSED
|
||||
if last_state := await self.async_get_last_state():
|
||||
self._last_state = last_state.state
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -27,19 +27,21 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, LIGHT)
|
||||
)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[LIGHT])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[LIGHT].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):
|
||||
|
@@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -66,22 +65,24 @@ async def async_setup_bridge_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_BRIDGE_TYPES
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, OTHER)
|
||||
)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[OTHER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_BRIDGE_TYPES
|
||||
for device in coordinator.data[OTHER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
async def async_setup_vedo_entry(
|
||||
@@ -93,22 +94,24 @@ async def async_setup_vedo_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -28,20 +28,35 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device in new_devices
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
entities: list[ComelitSwitchEntity] = []
|
||||
entities.extend(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[IRRIGATION].values()
|
||||
)
|
||||
entities.extend(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[OTHER].values()
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
for dev_type in (IRRIGATION, OTHER):
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, dev_type)
|
||||
)
|
||||
known_devices: dict[str, set[int]] = {
|
||||
dev_type: set() for dev_type in (IRRIGATION, OTHER)
|
||||
}
|
||||
|
||||
def _check_device() -> None:
|
||||
for dev_type in (IRRIGATION, OTHER):
|
||||
current_devices = set(coordinator.data[dev_type])
|
||||
new_devices = current_devices - known_devices[dev_type]
|
||||
if new_devices:
|
||||
known_devices[dev_type].update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
||||
|
@@ -4,11 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiocomelit.api import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
@@ -23,11 +19,8 @@ from homeassistant.helpers import (
|
||||
)
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import ComelitBaseCoordinator
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
|
||||
DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
|
||||
|
||||
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
@@ -120,41 +113,3 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
|
||||
return cmd_wrapper
|
||||
|
||||
|
||||
def new_device_listener(
|
||||
coordinator: ComelitBaseCoordinator,
|
||||
new_devices_callback: Callable[
|
||||
[
|
||||
list[
|
||||
ComelitSerialBridgeObject
|
||||
| ComelitVedoAreaObject
|
||||
| ComelitVedoZoneObject
|
||||
],
|
||||
str,
|
||||
],
|
||||
None,
|
||||
],
|
||||
data_type: str,
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to coordinator updates to check for new devices."""
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_devices() -> None:
|
||||
"""Check for new devices and call callback with any new monitors."""
|
||||
if not coordinator.data:
|
||||
return
|
||||
|
||||
new_devices: list[DeviceType] = []
|
||||
for _id in coordinator.data[data_type]:
|
||||
if _id not in known_devices:
|
||||
known_devices.add(_id)
|
||||
new_devices.append(coordinator.data[data_type][_id])
|
||||
|
||||
if new_devices:
|
||||
new_devices_callback(new_devices, data_type)
|
||||
|
||||
# Check for devices immediately
|
||||
_check_devices()
|
||||
|
||||
return coordinator.async_add_listener(_check_devices)
|
||||
|
@@ -45,18 +45,13 @@ from home_assistant_intents import (
|
||||
)
|
||||
import yaml
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_listen_entity_updates,
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
@@ -197,7 +192,7 @@ class IntentCache:
|
||||
|
||||
|
||||
async def async_setup_default_agent(
|
||||
hass: HomeAssistant,
|
||||
hass: core.HomeAssistant,
|
||||
entity_component: EntityComponent[ConversationEntity],
|
||||
config_intents: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -206,13 +201,15 @@ async def async_setup_default_agent(
|
||||
await entity_component.async_add_entities([agent])
|
||||
await get_agent_manager(hass).async_setup_default_agent(agent)
|
||||
|
||||
@callback
|
||||
def async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
|
||||
@core.callback
|
||||
def async_entity_state_listener(
|
||||
event: core.Event[core.EventStateChangedData],
|
||||
) -> None:
|
||||
"""Set expose flag on new entities."""
|
||||
async_should_expose(hass, DOMAIN, event.data["entity_id"])
|
||||
|
||||
@callback
|
||||
def async_hass_started(hass: HomeAssistant) -> None:
|
||||
@core.callback
|
||||
def async_hass_started(hass: core.HomeAssistant) -> None:
|
||||
"""Set expose flag on all entities."""
|
||||
for state in hass.states.async_all():
|
||||
async_should_expose(hass, DOMAIN, state.entity_id)
|
||||
@@ -227,7 +224,9 @@ class DefaultAgent(ConversationEntity):
|
||||
_attr_name = "Home Assistant"
|
||||
_attr_supported_features = ConversationEntityFeature.CONTROL
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_intents: dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, hass: core.HomeAssistant, config_intents: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize the default agent."""
|
||||
self.hass = hass
|
||||
self._lang_intents: dict[str, LanguageIntents | object] = {}
|
||||
@@ -260,7 +259,7 @@ class DefaultAgent(ConversationEntity):
|
||||
"""Return a list of supported languages."""
|
||||
return get_languages()
|
||||
|
||||
@callback
|
||||
@core.callback
|
||||
def _filter_entity_registry_changes(
|
||||
self, event_data: er.EventEntityRegistryUpdatedData
|
||||
) -> bool:
|
||||
@@ -269,12 +268,12 @@ class DefaultAgent(ConversationEntity):
|
||||
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
|
||||
)
|
||||
|
||||
@callback
|
||||
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
|
||||
@core.callback
|
||||
def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool:
|
||||
"""Filter state changed events."""
|
||||
return not event_data["old_state"] or not event_data["new_state"]
|
||||
|
||||
@callback
|
||||
@core.callback
|
||||
def _listen_clear_slot_list(self) -> None:
|
||||
"""Listen for changes that can invalidate slot list."""
|
||||
assert self._unsub_clear_slot_list is None
|
||||
@@ -891,7 +890,7 @@ class DefaultAgent(ConversationEntity):
|
||||
) -> str:
|
||||
# Get first matched or unmatched state.
|
||||
# This is available in the response template as "state".
|
||||
state1: State | None = None
|
||||
state1: core.State | None = None
|
||||
if intent_response.matched_states:
|
||||
state1 = intent_response.matched_states[0]
|
||||
elif intent_response.unmatched_states:
|
||||
@@ -1590,7 +1589,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
|
||||
|
||||
def _get_match_error_response(
|
||||
hass: HomeAssistant,
|
||||
hass: core.HomeAssistant,
|
||||
match_error: intent.MatchFailedError,
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when target matching fails."""
|
||||
|
@@ -116,10 +116,6 @@ class WaterSourceType(TypedDict):
|
||||
# an EnergyCostSensor will be automatically created
|
||||
stat_cost: str | None
|
||||
|
||||
# An optional statistic_id identifying a device
|
||||
# that includes this device's consumption in its total
|
||||
included_in_stat: str | None
|
||||
|
||||
# Used to generate costs if stat_cost is set to None
|
||||
entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
|
||||
number_energy_price: float | None # Price for energy ($/m³)
|
||||
|
@@ -41,12 +41,16 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||
self._expected_connected = False
|
||||
self._height: int | None = None
|
||||
|
||||
@callback
|
||||
def async_update_data() -> None:
|
||||
self.async_set_updated_data(self._height)
|
||||
|
||||
self._debouncer = Debouncer(
|
||||
hass=self.hass,
|
||||
logger=_LOGGER,
|
||||
cooldown=UPDATE_DEBOUNCE_TIME,
|
||||
immediate=True,
|
||||
function=callback(lambda: self.async_set_updated_data(self._height)),
|
||||
function=async_update_data,
|
||||
)
|
||||
|
||||
async def async_connect(self) -> bool:
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -24,13 +23,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
|
||||
from . import async_get_config_entry_implementation
|
||||
from .application_credentials import authorization_server_context
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
|
||||
from .coordinator import TokenManager, mcp_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -48,17 +41,9 @@ MCP_DISCOVERY_HEADERS = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OAuthConfig:
|
||||
"""Class to hold OAuth configuration."""
|
||||
|
||||
authorization_server: AuthorizationServer
|
||||
scopes: list[str] | None = None
|
||||
|
||||
|
||||
async def async_discover_oauth_config(
|
||||
hass: HomeAssistant, mcp_server_url: str
|
||||
) -> OAuthConfig:
|
||||
) -> AuthorizationServer:
|
||||
"""Discover the OAuth configuration for the MCP server.
|
||||
|
||||
This implements the functionality in the MCP spec for discovery. If the MCP server URL
|
||||
@@ -80,11 +65,9 @@ async def async_discover_oauth_config(
|
||||
except httpx.HTTPStatusError as error:
|
||||
if error.response.status_code == 404:
|
||||
_LOGGER.info("Authorization Server Metadata not found, using default paths")
|
||||
return OAuthConfig(
|
||||
authorization_server=AuthorizationServer(
|
||||
authorize_url=str(parsed_url.with_path("/authorize")),
|
||||
token_url=str(parsed_url.with_path("/token")),
|
||||
)
|
||||
return AuthorizationServer(
|
||||
authorize_url=str(parsed_url.with_path("/authorize")),
|
||||
token_url=str(parsed_url.with_path("/token")),
|
||||
)
|
||||
raise CannotConnect from error
|
||||
except httpx.HTTPError as error:
|
||||
@@ -98,15 +81,9 @@ async def async_discover_oauth_config(
|
||||
authorize_url = str(parsed_url.with_path(authorize_url))
|
||||
if token_url.startswith("/"):
|
||||
token_url = str(parsed_url.with_path(token_url))
|
||||
# We have no way to know the minimum set of scopes needed, so request
|
||||
# all of them and let the user limit during the authorization step.
|
||||
scopes = data.get("scopes_supported")
|
||||
return OAuthConfig(
|
||||
authorization_server=AuthorizationServer(
|
||||
authorize_url=authorize_url,
|
||||
token_url=token_url,
|
||||
),
|
||||
scopes=scopes,
|
||||
return AuthorizationServer(
|
||||
authorize_url=authorize_url,
|
||||
token_url=token_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -153,7 +130,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
super().__init__()
|
||||
self.data: dict[str, Any] = {}
|
||||
self.oauth_config: OAuthConfig | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -194,7 +170,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
to find the OAuth medata then run the OAuth authentication flow.
|
||||
"""
|
||||
try:
|
||||
oauth_config = await async_discover_oauth_config(
|
||||
authorization_server = await async_discover_oauth_config(
|
||||
self.hass, self.data[CONF_URL]
|
||||
)
|
||||
except TimeoutConnectError:
|
||||
@@ -205,13 +181,11 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
else:
|
||||
_LOGGER.info("OAuth configuration: %s", oauth_config)
|
||||
self.oauth_config = oauth_config
|
||||
_LOGGER.info("OAuth configuration: %s", authorization_server)
|
||||
self.data.update(
|
||||
{
|
||||
CONF_AUTHORIZATION_URL: oauth_config.authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: oauth_config.authorization_server.token_url,
|
||||
CONF_SCOPE: oauth_config.scopes,
|
||||
CONF_AUTHORIZATION_URL: authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: authorization_server.token_url,
|
||||
}
|
||||
)
|
||||
return await self.async_step_credentials_choice()
|
||||
@@ -223,15 +197,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self.data[CONF_TOKEN_URL],
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data = {}
|
||||
if self.data and (scopes := self.data[CONF_SCOPE]) is not None:
|
||||
data[CONF_SCOPE] = " ".join(scopes)
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
|
||||
async def async_step_credentials_choice(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@@ -5,4 +5,3 @@ DOMAIN = "mcp"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_AUTHORIZATION_URL = "authorization_url"
|
||||
CONF_TOKEN_URL = "token_url"
|
||||
CONF_SCOPE = "scope"
|
||||
|
@@ -54,7 +54,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PLATE_COUNT = 4
|
||||
|
||||
PLATE_COUNT = {
|
||||
"KM7575": 6,
|
||||
"KM7678": 6,
|
||||
"KM7697": 6,
|
||||
"KM7878": 6,
|
||||
|
@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
) from error
|
||||
except NordPoolEmptyResponseError:
|
||||
return {area: [] for area in areas}
|
||||
except (NordPoolError, TimeoutError) as error:
|
||||
except NordPoolError as error:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
|
@@ -31,39 +31,38 @@ async def async_setup_entry(
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(
|
||||
ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
)
|
||||
entities = {
|
||||
event.uid: ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
|
||||
if entry.domain == "binary_sensor" and entry.unique_id not in uids:
|
||||
uids.add(entry.unique_id)
|
||||
entities.append(ONVIFBinarySensor(entry.unique_id, device, entry=entry))
|
||||
if entry.domain == "binary_sensor" and entry.unique_id not in entities:
|
||||
entities[entry.unique_id] = ONVIFBinarySensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities.values())
|
||||
uids_by_platform = device.events.get_uids_by_platform("binary_sensor")
|
||||
|
||||
@callback
|
||||
def async_check_entities() -> None:
|
||||
"""Check if we have added an entity for the event."""
|
||||
nonlocal uids_by_platform
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
if not (missing := uids_by_platform.difference(entities)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities = [
|
||||
ONVIFBinarySensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
new_entities: dict[str, ONVIFBinarySensor] = {
|
||||
uid: ONVIFBinarySensor(uid, device, name=entity_names[uid])
|
||||
for uid in missing
|
||||
}
|
||||
if new_entities:
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
|
||||
device.events.async_add_listener(async_check_entities)
|
||||
|
||||
|
@@ -30,37 +30,37 @@ async def async_setup_entry(
|
||||
events = device.events.get_platform("sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(ONVIFSensor(event.uid, device, name=entity_names[event.uid]))
|
||||
entities = {
|
||||
event.uid: ONVIFSensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
|
||||
if entry.domain == "sensor" and entry.unique_id not in uids:
|
||||
uids.add(entry.unique_id)
|
||||
entities.append(ONVIFSensor(entry.unique_id, device, entry=entry))
|
||||
if entry.domain == "sensor" and entry.unique_id not in entities:
|
||||
entities[entry.unique_id] = ONVIFSensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities.values())
|
||||
uids_by_platform = device.events.get_uids_by_platform("sensor")
|
||||
|
||||
@callback
|
||||
def async_check_entities() -> None:
|
||||
"""Check if we have added an entity for the event."""
|
||||
nonlocal uids_by_platform
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
if not (missing := uids_by_platform.difference(entities)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities = [
|
||||
ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
new_entities: dict[str, ONVIFSensor] = {
|
||||
uid: ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
}
|
||||
if new_entities:
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
|
||||
device.events.async_add_listener(async_check_entities)
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import logging
|
||||
from pyopenweathermap import create_owm_client
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS
|
||||
@@ -25,6 +25,7 @@ type OpenweathermapConfigEntry = ConfigEntry[OpenweathermapData]
|
||||
class OpenweathermapData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
name: str
|
||||
mode: str
|
||||
coordinator: OWMUpdateCoordinator
|
||||
|
||||
@@ -33,6 +34,7 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: OpenweathermapConfigEntry
|
||||
) -> bool:
|
||||
"""Set up OpenWeatherMap as config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
language = entry.options[CONF_LANGUAGE]
|
||||
mode = entry.options[CONF_MODE]
|
||||
@@ -49,7 +51,7 @@ async def async_setup_entry(
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
entry.runtime_data = OpenweathermapData(mode, owm_coordinator)
|
||||
entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@@ -14,17 +14,12 @@ from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
LanguageSelector,
|
||||
LanguageSelectorConfig,
|
||||
LocationSelector,
|
||||
LocationSelectorConfig,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONFIG_FLOW_VERSION,
|
||||
@@ -39,12 +34,10 @@ from .utils import build_data_and_options, validate_api_key
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LOCATION): LocationSelector(
|
||||
LocationSelectorConfig(radius=False)
|
||||
),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
@@ -52,9 +45,7 @@ USER_SCHEMA = vol.Schema(
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
)
|
||||
@@ -79,8 +70,8 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LOCATION][CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LOCATION][CONF_LONGITUDE]
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
mode = user_input[CONF_MODE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
@@ -91,21 +82,15 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
if not errors:
|
||||
# Flatten location
|
||||
location = user_input.pop(CONF_LOCATION)
|
||||
user_input[CONF_LATITUDE] = location[CONF_LATITUDE]
|
||||
user_input[CONF_LONGITUDE] = location[CONF_LONGITUDE]
|
||||
data, options = build_data_and_options(user_input)
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME, data=data, options=options
|
||||
title=user_input[CONF_NAME], data=data, options=options
|
||||
)
|
||||
schema_data = user_input
|
||||
else:
|
||||
schema_data = {
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
},
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
CONF_LANGUAGE: self.hass.config.language,
|
||||
}
|
||||
|
||||
|
@@ -121,7 +121,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CLOUDS,
|
||||
@@ -159,7 +158,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CONDITION,
|
||||
@@ -229,6 +227,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up OpenWeatherMap sensor entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
unique_id = config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
coordinator = domain_data.coordinator
|
||||
@@ -243,6 +242,7 @@ async def async_setup_entry(
|
||||
elif domain_data.mode == OWM_MODE_AIRPOLLUTION:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -252,6 +252,7 @@ async def async_setup_entry(
|
||||
else:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -269,6 +270,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: OWMUpdateCoordinator,
|
||||
@@ -282,6 +284,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -12,14 +12,16 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"mode": "[%key:common::config_flow::data::mode%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API key for the OpenWeatherMap integration",
|
||||
"language": "Language for the OpenWeatherMap content",
|
||||
"location": "Location to get the weather data for",
|
||||
"latitude": "Latitude of the location",
|
||||
"longitude": "Longitude of the location",
|
||||
"mode": "Mode for the OpenWeatherMap API",
|
||||
"name": "Name for this OpenWeatherMap location"
|
||||
},
|
||||
|
@@ -57,13 +57,14 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up OpenWeatherMap weather entity based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
mode = domain_data.mode
|
||||
|
||||
if mode != OWM_MODE_AIRPOLLUTION:
|
||||
weather_coordinator = domain_data.coordinator
|
||||
|
||||
unique_id = f"{config_entry.unique_id}"
|
||||
owm_weather = OpenWeatherMapWeather(unique_id, mode, weather_coordinator)
|
||||
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator)
|
||||
|
||||
async_add_entities([owm_weather], False)
|
||||
|
||||
@@ -92,6 +93,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
mode: str,
|
||||
weather_coordinator: OWMUpdateCoordinator,
|
||||
@@ -103,6 +105,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
)
|
||||
self.mode = mode
|
||||
|
||||
|
@@ -48,7 +48,7 @@ from .const import (
|
||||
|
||||
DEFAULT_OFF_DELAY = 2.0
|
||||
|
||||
CONNECT_TIMEOUT = 60.0
|
||||
CONNECT_TIMEOUT = 30.0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -24,7 +24,6 @@
|
||||
},
|
||||
"config_subentries": {
|
||||
"partition": {
|
||||
"entry_type": "Partition",
|
||||
"initiate_flow": {
|
||||
"user": "Add partition"
|
||||
},
|
||||
@@ -58,7 +57,6 @@
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"entry_type": "Zone",
|
||||
"initiate_flow": {
|
||||
"user": "Add zone"
|
||||
},
|
||||
@@ -93,7 +91,6 @@
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"entry_type": "Output",
|
||||
"initiate_flow": {
|
||||
"user": "Add output"
|
||||
},
|
||||
@@ -128,7 +125,6 @@
|
||||
}
|
||||
},
|
||||
"switchable_output": {
|
||||
"entry_type": "Switchable output",
|
||||
"initiate_flow": {
|
||||
"user": "Add switchable output"
|
||||
},
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from aioshelly.ble.const import BLE_SCRIPT_NAME
|
||||
@@ -64,7 +63,6 @@ from .repairs import (
|
||||
)
|
||||
from .utils import (
|
||||
async_create_issue_unsupported_firmware,
|
||||
async_migrate_rpc_virtual_components_unique_ids,
|
||||
get_coap_context,
|
||||
get_device_entry_gen,
|
||||
get_http_port,
|
||||
@@ -325,12 +323,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
translation_placeholders={"device": entry.title},
|
||||
) from err
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
partial(async_migrate_rpc_virtual_components_unique_ids, device.config),
|
||||
)
|
||||
|
||||
runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
|
||||
runtime_data.rpc.async_setup()
|
||||
runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE
|
||||
from .const import CONF_SLEEP_PERIOD
|
||||
from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -270,21 +270,12 @@ RPC_SENSORS: Final = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"boolean_generic": RpcBinarySensorDescription(
|
||||
"boolean": RpcBinarySensorDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, BINARY_SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
"boolean_has_power": RpcBinarySensorDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
role="has_power",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"calibration": RpcBinarySensorDescription(
|
||||
key="blutrv",
|
||||
|
@@ -308,5 +308,3 @@ MODEL_NEO_WATER_VALVE = "NeoWaterValve"
|
||||
MODEL_FRANKEVER_WATER_VALVE = "WaterValve"
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT = "ST-802"
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT = "ST1820"
|
||||
MODEL_TOP_EV_CHARGER_EVE01 = "EVE01"
|
||||
MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation"
|
||||
|
@@ -29,7 +29,6 @@ from .utils import (
|
||||
get_rpc_device_info,
|
||||
get_rpc_entity_name,
|
||||
get_rpc_key_instances,
|
||||
get_rpc_role_by_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,9 +189,9 @@ def async_setup_rpc_attribute_entities(
|
||||
if description.models and coordinator.model not in description.models:
|
||||
continue
|
||||
|
||||
if description.role and description.role != get_rpc_role_by_key(
|
||||
coordinator.device.config, key
|
||||
):
|
||||
if description.role and description.role != coordinator.device.config[
|
||||
key
|
||||
].get("role", "generic"):
|
||||
continue
|
||||
|
||||
if description.sub_key not in coordinator.device.status[
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioshelly==13.12.0"],
|
||||
"requirements": ["aioshelly==13.11.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@@ -24,16 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
from .const import (
|
||||
CONF_SLEEP_PERIOD,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MODEL_FRANKEVER_WATER_VALVE,
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT,
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT,
|
||||
MODEL_TOP_EV_CHARGER_EVE01,
|
||||
VIRTUAL_NUMBER_MODE_MAP,
|
||||
)
|
||||
from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER, VIRTUAL_NUMBER_MODE_MAP
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -192,7 +183,7 @@ RPC_NUMBERS: Final = {
|
||||
method="blu_trv_set_external_temperature",
|
||||
entity_class=RpcBluTrvExtTempNumber,
|
||||
),
|
||||
"number_generic": RpcNumberDescription(
|
||||
"number": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -206,58 +197,6 @@ RPC_NUMBERS: Final = {
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="generic",
|
||||
),
|
||||
"number_current_limit": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="current_limit",
|
||||
models={MODEL_TOP_EV_CHARGER_EVE01},
|
||||
),
|
||||
"number_position": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="position",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"number_target_humidity": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="target_humidity",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"number_target_temperature": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: NumberMode.SLIDER,
|
||||
step_fn=lambda config: config["meta"]["ui"].get("step"),
|
||||
unit=get_virtual_component_unit,
|
||||
method="number_set",
|
||||
role="target_temperature",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"valve_position": RpcNumberDescription(
|
||||
key="blutrv",
|
||||
|
@@ -38,13 +38,12 @@ class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
|
||||
|
||||
|
||||
RPC_SELECT_ENTITIES: Final = {
|
||||
"enum_generic": RpcSelectDescription(
|
||||
"enum": RpcSelectDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SELECT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, cast
|
||||
from functools import partial
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
@@ -36,12 +37,13 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD
|
||||
from .const import CONF_SLEEP_PERIOD, LOGGER
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -549,7 +551,7 @@ RPC_SENSORS: Final = {
|
||||
"a_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="a_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -559,7 +561,7 @@ RPC_SENSORS: Final = {
|
||||
"b_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="b_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -569,7 +571,7 @@ RPC_SENSORS: Final = {
|
||||
"c_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="c_act_power",
|
||||
name="Power",
|
||||
name="Active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -579,7 +581,7 @@ RPC_SENSORS: Final = {
|
||||
"total_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_act_power",
|
||||
name="Power",
|
||||
name="Total active power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -625,7 +627,7 @@ RPC_SENSORS: Final = {
|
||||
"total_aprt_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_aprt_power",
|
||||
name="Apparent power",
|
||||
name="Total apparent power",
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -880,7 +882,7 @@ RPC_SENSORS: Final = {
|
||||
"n_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="n_current",
|
||||
name="Neutral current",
|
||||
name="Phase N current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -890,7 +892,7 @@ RPC_SENSORS: Final = {
|
||||
"total_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_current",
|
||||
name="Current",
|
||||
name="Total current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -1382,7 +1384,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement="pulse",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value=lambda status, _: status["total"],
|
||||
removal_condition=lambda config, _, key: (
|
||||
removal_condition=lambda config, _status, key: (
|
||||
config[key]["type"] != "count" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
@@ -1422,7 +1424,7 @@ RPC_SENSORS: Final = {
|
||||
"text_generic": RpcSensorDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
@@ -1430,7 +1432,7 @@ RPC_SENSORS: Final = {
|
||||
"number_generic": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
unit=get_virtual_component_unit,
|
||||
@@ -1439,7 +1441,7 @@ RPC_SENSORS: Final = {
|
||||
"enum_generic": RpcSensorDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
options_fn=lambda config: config["options"],
|
||||
@@ -1454,7 +1456,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
removal_condition=lambda config, _, key: config[key].get("enable", False)
|
||||
removal_condition=lambda config, _status, key: config[key].get("enable", False)
|
||||
is False,
|
||||
entity_class=RpcBluTrvSensor,
|
||||
),
|
||||
@@ -1604,7 +1606,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_act_energy": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Energy",
|
||||
name="Total Active Energy",
|
||||
value=lambda status, _: float(status["total_act_energy"]),
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1616,7 +1618,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Power",
|
||||
name="Total Power",
|
||||
value=lambda status, _: float(status["total_power"]),
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
@@ -1661,6 +1663,39 @@ RPC_SENSORS: Final = {
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_unique_ids(
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate sensor unique IDs to include role."""
|
||||
if not entity_entry.entity_id.startswith("sensor."):
|
||||
return None
|
||||
|
||||
for sensor_id in ("text", "number", "enum"):
|
||||
old_unique_id = entity_entry.unique_id
|
||||
if old_unique_id.endswith(f"-{sensor_id}"):
|
||||
if entity_entry.original_device_class == SensorDeviceClass.HUMIDITY:
|
||||
new_unique_id = f"{old_unique_id}_current_humidity"
|
||||
elif entity_entry.original_device_class == SensorDeviceClass.TEMPERATURE:
|
||||
new_unique_id = f"{old_unique_id}_current_temperature"
|
||||
else:
|
||||
new_unique_id = f"{old_unique_id}_generic"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ShellyConfigEntry,
|
||||
@@ -1680,6 +1715,12 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
assert coordinator
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
partial(async_migrate_unique_ids, coordinator),
|
||||
)
|
||||
|
||||
async_setup_entry_rpc(
|
||||
hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
|
||||
)
|
||||
|
@@ -21,13 +21,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
|
||||
MODEL_LINKEDGO_ST802_THERMOSTAT,
|
||||
MODEL_LINKEDGO_ST1820_THERMOSTAT,
|
||||
MODEL_NEO_WATER_VALVE,
|
||||
MODEL_TOP_EV_CHARGER_EVE01,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -94,7 +87,7 @@ RPC_RELAY_SWITCHES = {
|
||||
}
|
||||
|
||||
RPC_SWITCHES = {
|
||||
"boolean_generic": RpcSwitchDescription(
|
||||
"boolean": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -104,120 +97,6 @@ RPC_SWITCHES = {
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="generic",
|
||||
),
|
||||
"boolean_anti_freeze": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="anti_freeze",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_child_lock": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="child_lock",
|
||||
models={MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_enable": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="enable",
|
||||
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
|
||||
),
|
||||
"boolean_start_charging": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="start_charging",
|
||||
models={MODEL_TOP_EV_CHARGER_EVE01},
|
||||
),
|
||||
"boolean_state": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
entity_registry_enabled_default=False,
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="state",
|
||||
models={MODEL_NEO_WATER_VALVE},
|
||||
),
|
||||
"boolean_zone0": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone0",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone1": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone1",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone2": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone2",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone3": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone3",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone4": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone4",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"boolean_zone5": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
is_on=lambda status: bool(status["value"]),
|
||||
method_on="Boolean.Set",
|
||||
method_off="Boolean.Set",
|
||||
method_params_fn=lambda id, value: {"id": id, "value": value},
|
||||
role="zone5",
|
||||
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
|
||||
),
|
||||
"script": RpcSwitchDescription(
|
||||
key="script",
|
||||
|
@@ -38,13 +38,12 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription):
|
||||
|
||||
|
||||
RPC_TEXT_ENTITIES: Final = {
|
||||
"text_generic": RpcTextDescription(
|
||||
"text": RpcTextDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, TEXT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -484,11 +484,6 @@ def get_rpc_key_by_role(keys_dict: dict[str, Any], role: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_rpc_role_by_key(keys_dict: dict[str, Any], key: str) -> str:
|
||||
"""Return role by key for RPC device from a dict."""
|
||||
return cast(str, keys_dict[key].get("role", "generic"))
|
||||
|
||||
|
||||
def id_from_key(key: str) -> int:
|
||||
"""Return id from key."""
|
||||
return int(key.split(":")[-1])
|
||||
@@ -939,35 +934,3 @@ def remove_empty_sub_devices(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
def format_ble_addr(ble_addr: str) -> str:
|
||||
"""Format BLE address to use in unique_id."""
|
||||
return ble_addr.replace(":", "").upper()
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_rpc_virtual_components_unique_ids(
|
||||
config: dict[str, Any], entity_entry: er.RegistryEntry
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate RPC virtual components unique_ids to include role in the ID.
|
||||
|
||||
This is needed to support multiple components with the same key.
|
||||
The old unique_id format is: {mac}-{key}-{component}
|
||||
The new unique_id format is: {mac}-{key}-{component}_{role}
|
||||
"""
|
||||
for component in VIRTUAL_COMPONENTS:
|
||||
if entity_entry.unique_id.endswith(f"-{component!s}"):
|
||||
key = entity_entry.unique_id.split("-")[-2]
|
||||
if key not in config:
|
||||
continue
|
||||
role = get_rpc_role_by_key(config, key)
|
||||
new_unique_id = f"{entity_entry.unique_id}_{role}"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
entity_entry.unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
entity_entry.unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
@@ -179,13 +179,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
is_on_key="open",
|
||||
)
|
||||
},
|
||||
Capability.GAS_DETECTOR: {
|
||||
Attribute.GAS: SmartThingsBinarySensorEntityDescription(
|
||||
key=Attribute.GAS,
|
||||
device_class=BinarySensorDeviceClass.GAS,
|
||||
is_on_key="detected",
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@@ -530,6 +530,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ILLUMINANCE_MEASUREMENT: {
|
||||
Attribute.ILLUMINANCE: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -841,6 +842,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.SIGNAL_STRENGTH: {
|
||||
Attribute.LQI: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -999,6 +1001,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.TVOC_MEASUREMENT: {
|
||||
Attribute.TVOC_LEVEL: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -1009,6 +1012,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ULTRAVIOLET_INDEX: {
|
||||
Attribute.ULTRAVIOLET_INDEX: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -20,7 +21,7 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@@ -454,8 +455,28 @@ async def async_attach_trigger(
|
||||
zwave_js_config = await validate_value_updated_trigger_config(
|
||||
hass, zwave_js_config
|
||||
)
|
||||
|
||||
@callback
|
||||
def run_action(
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Run action with trigger variables."""
|
||||
|
||||
payload = {
|
||||
"trigger": {
|
||||
**trigger_info["trigger_data"],
|
||||
CONF_PLATFORM: VALUE_UPDATED_PLATFORM_TYPE,
|
||||
"description": description,
|
||||
**extra_trigger_payload,
|
||||
}
|
||||
}
|
||||
|
||||
return hass.async_create_task(action(payload, context))
|
||||
|
||||
return await attach_value_updated_trigger(
|
||||
hass, zwave_js_config[CONF_OPTIONS], action, trigger_info
|
||||
hass, zwave_js_config[CONF_OPTIONS], run_action
|
||||
)
|
||||
|
||||
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
|
||||
|
@@ -134,6 +134,7 @@ ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription]
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
|
@@ -17,19 +17,12 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_PLATFORM,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerData,
|
||||
TriggerInfo,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..const import (
|
||||
@@ -127,17 +120,13 @@ _CONFIG_SCHEMA = vol.Schema(
|
||||
class EventTrigger(Trigger):
|
||||
"""Z-Wave JS event trigger."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_options: dict[str, Any]
|
||||
|
||||
_event_source: str
|
||||
_event_name: str
|
||||
_event_data_filter: dict
|
||||
_job: HassJob
|
||||
_trigger_data: TriggerData
|
||||
_unsubs: list[Callable]
|
||||
|
||||
_platform_type = PLATFORM_TYPE
|
||||
_action_runner: TriggerActionRunner
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
@@ -176,14 +165,12 @@ class EventTrigger(Trigger):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
self._hass = hass
|
||||
super().__init__(hass, config)
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
dev_reg = dr.async_get(self._hass)
|
||||
@@ -198,8 +185,7 @@ class EventTrigger(Trigger):
|
||||
self._event_source = options[ATTR_EVENT_SOURCE]
|
||||
self._event_name = options[ATTR_EVENT]
|
||||
self._event_data_filter = options.get(ATTR_EVENT_DATA, {})
|
||||
self._job = HassJob(action)
|
||||
self._trigger_data = trigger_info["trigger_data"]
|
||||
self._action_runner = run_action
|
||||
self._unsubs: list[Callable] = []
|
||||
|
||||
self._create_zwave_listeners()
|
||||
@@ -225,9 +211,7 @@ class EventTrigger(Trigger):
|
||||
if event_data[key] != val:
|
||||
return
|
||||
|
||||
payload = {
|
||||
**self._trigger_data,
|
||||
CONF_PLATFORM: self._platform_type,
|
||||
payload: dict[str, Any] = {
|
||||
ATTR_EVENT_SOURCE: self._event_source,
|
||||
ATTR_EVENT: self._event_name,
|
||||
ATTR_EVENT_DATA: event_data,
|
||||
@@ -237,21 +221,17 @@ class EventTrigger(Trigger):
|
||||
f"Z-Wave JS '{self._event_source}' event '{self._event_name}' was emitted"
|
||||
)
|
||||
|
||||
description = primary_desc
|
||||
if device:
|
||||
device_name = device.name_by_user or device.name
|
||||
payload[ATTR_DEVICE_ID] = device.id
|
||||
home_and_node_id = get_home_and_node_id_from_device_entry(device)
|
||||
assert home_and_node_id
|
||||
payload[ATTR_NODE_ID] = home_and_node_id[1]
|
||||
payload["description"] = f"{primary_desc} on {device_name}"
|
||||
else:
|
||||
payload["description"] = primary_desc
|
||||
description = f"{primary_desc} on {device_name}"
|
||||
|
||||
payload["description"] = (
|
||||
f"{payload['description']} with event data: {event_data}"
|
||||
)
|
||||
|
||||
self._hass.async_run_hass_job(self._job, {"trigger": payload})
|
||||
description = f"{description} with event data: {event_data}"
|
||||
self._action_runner(payload, description)
|
||||
|
||||
@callback
|
||||
def _async_remove(self) -> None:
|
||||
|
@@ -11,23 +11,12 @@ from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.value import Value, get_value_id_str
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_PLATFORM,
|
||||
MATCH_ALL,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_OPTIONS, MATCH_ALL
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerInfo,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from ..config_validation import VALUE_SCHEMA
|
||||
@@ -100,12 +89,7 @@ async def async_validate_trigger_config(
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
options: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
*,
|
||||
platform_type: str = PLATFORM_TYPE,
|
||||
hass: HomeAssistant, options: ConfigType, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
@@ -121,9 +105,6 @@ async def async_attach_trigger(
|
||||
endpoint = options.get(ATTR_ENDPOINT)
|
||||
property_key = options.get(ATTR_PROPERTY_KEY)
|
||||
unsubs: list[Callable] = []
|
||||
job = HassJob(action)
|
||||
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
|
||||
@callback
|
||||
def async_on_value_updated(
|
||||
@@ -152,10 +133,8 @@ async def async_attach_trigger(
|
||||
return
|
||||
|
||||
device_name = device.name_by_user or device.name
|
||||
|
||||
description = f"Z-Wave value {value.value_id} updated on {device_name}"
|
||||
payload = {
|
||||
**trigger_data,
|
||||
CONF_PLATFORM: platform_type,
|
||||
ATTR_DEVICE_ID: device.id,
|
||||
ATTR_NODE_ID: value.node.node_id,
|
||||
ATTR_COMMAND_CLASS: value.command_class,
|
||||
@@ -169,10 +148,9 @@ async def async_attach_trigger(
|
||||
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
|
||||
ATTR_CURRENT_VALUE: curr_value,
|
||||
ATTR_CURRENT_VALUE_RAW: curr_value_raw,
|
||||
"description": f"Z-Wave value {value.value_id} updated on {device_name}",
|
||||
}
|
||||
|
||||
hass.async_run_hass_job(job, {"trigger": payload})
|
||||
run_action(payload, description)
|
||||
|
||||
@callback
|
||||
def async_remove() -> None:
|
||||
@@ -223,7 +201,6 @@ async def async_attach_trigger(
|
||||
class ValueUpdatedTrigger(Trigger):
|
||||
"""Z-Wave JS value updated trigger."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_options: dict[str, Any]
|
||||
|
||||
@classmethod
|
||||
@@ -245,16 +222,12 @@ class ValueUpdatedTrigger(Trigger):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
self._hass = hass
|
||||
super().__init__(hass, config)
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
return await async_attach_trigger(
|
||||
self._hass, self._options, action, trigger_info
|
||||
)
|
||||
return await async_attach_trigger(self._hass, self._options, run_action)
|
||||
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Final
|
||||
|
||||
from .generated.entity_platforms import EntityPlatforms
|
||||
from .helpers.deprecation import (
|
||||
DeprecatedConstant,
|
||||
DeprecatedConstantEnum,
|
||||
EnumWithDeprecatedMembers,
|
||||
all_with_deprecated_constants,
|
||||
@@ -315,6 +316,60 @@ STATE_OK: Final = "ok"
|
||||
STATE_PROBLEM: Final = "problem"
|
||||
|
||||
|
||||
# #### ALARM CONTROL PANEL STATES ####
|
||||
# STATE_ALARM_* below are deprecated as of 2024.11
|
||||
# use the AlarmControlPanelState enum instead.
|
||||
_DEPRECATED_STATE_ALARM_DISARMED: Final = DeprecatedConstant(
|
||||
"disarmed",
|
||||
"AlarmControlPanelState.DISARMED",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMED_HOME: Final = DeprecatedConstant(
|
||||
"armed_home",
|
||||
"AlarmControlPanelState.ARMED_HOME",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMED_AWAY: Final = DeprecatedConstant(
|
||||
"armed_away",
|
||||
"AlarmControlPanelState.ARMED_AWAY",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMED_NIGHT: Final = DeprecatedConstant(
|
||||
"armed_night",
|
||||
"AlarmControlPanelState.ARMED_NIGHT",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMED_VACATION: Final = DeprecatedConstant(
|
||||
"armed_vacation",
|
||||
"AlarmControlPanelState.ARMED_VACATION",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = DeprecatedConstant(
|
||||
"armed_custom_bypass",
|
||||
"AlarmControlPanelState.ARMED_CUSTOM_BYPASS",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_PENDING: Final = DeprecatedConstant(
|
||||
"pending",
|
||||
"AlarmControlPanelState.PENDING",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_ARMING: Final = DeprecatedConstant(
|
||||
"arming",
|
||||
"AlarmControlPanelState.ARMING",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_DISARMING: Final = DeprecatedConstant(
|
||||
"disarming",
|
||||
"AlarmControlPanelState.DISARMING",
|
||||
"2025.11",
|
||||
)
|
||||
_DEPRECATED_STATE_ALARM_TRIGGERED: Final = DeprecatedConstant(
|
||||
"triggered",
|
||||
"AlarmControlPanelState.TRIGGERED",
|
||||
"2025.11",
|
||||
)
|
||||
|
||||
# #### STATE AND EVENT ATTRIBUTES ####
|
||||
# Attribution
|
||||
ATTR_ATTRIBUTION: Final = "attribution"
|
||||
|
@@ -28,8 +28,10 @@ from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Context,
|
||||
HassJob,
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
get_hassjob_callable_job_type,
|
||||
is_callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||
@@ -178,6 +180,8 @@ _TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
class Trigger(abc.ABC):
|
||||
"""Trigger class."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
@@ -212,14 +216,33 @@ class Trigger(abc.ABC):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
self._hass = hass
|
||||
|
||||
async def async_attach_action(
|
||||
self,
|
||||
action: TriggerAction,
|
||||
action_payload_builder: TriggerActionPayloadBuilder,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action."""
|
||||
|
||||
@callback
|
||||
def run_action(
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Run action with trigger variables."""
|
||||
|
||||
payload = action_payload_builder(extra_trigger_payload, description)
|
||||
return self._hass.async_create_task(action(payload, context))
|
||||
|
||||
return await self.async_attach_runner(run_action)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger."""
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
|
||||
class TriggerProtocol(Protocol):
|
||||
@@ -257,7 +280,33 @@ class TriggerConfig:
|
||||
options: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class TriggerActionType(Protocol):
|
||||
class TriggerActionRunner(Protocol):
|
||||
"""Protocol type for the trigger action runner helper callback."""
|
||||
|
||||
@callback
|
||||
def __call__(
|
||||
self,
|
||||
extra_trigger_payload: dict[str, Any],
|
||||
description: str,
|
||||
context: Context | None = None,
|
||||
) -> asyncio.Task[Any]:
|
||||
"""Define trigger action runner type.
|
||||
|
||||
Returns:
|
||||
A Task that allows awaiting for the action to finish.
|
||||
"""
|
||||
|
||||
|
||||
class TriggerActionPayloadBuilder(Protocol):
|
||||
"""Protocol type for the trigger action payload builder."""
|
||||
|
||||
def __call__(
|
||||
self, extra_trigger_payload: dict[str, Any], description: str
|
||||
) -> dict[str, Any]:
|
||||
"""Define trigger action payload builder type."""
|
||||
|
||||
|
||||
class TriggerAction(Protocol):
|
||||
"""Protocol type for trigger action callback."""
|
||||
|
||||
async def __call__(
|
||||
@@ -268,6 +317,20 @@ class TriggerActionType(Protocol):
|
||||
"""Define action callback type."""
|
||||
|
||||
|
||||
class TriggerActionType(Protocol):
|
||||
"""Protocol type for trigger action callback.
|
||||
|
||||
Contrary to TriggerAction, this type supports both sync and async callables.
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
run_variables: dict[str, Any],
|
||||
context: Context | None = None,
|
||||
) -> Coroutine[Any, Any, Any] | Any:
|
||||
"""Define action callback type."""
|
||||
|
||||
|
||||
class TriggerData(TypedDict):
|
||||
"""Trigger data."""
|
||||
|
||||
@@ -493,6 +556,73 @@ def _trigger_action_wrapper(
|
||||
return wrapper_func
|
||||
|
||||
|
||||
async def _async_attach_trigger_cls(
|
||||
hass: HomeAssistant,
|
||||
trigger_cls: type[Trigger],
|
||||
trigger_key: str,
|
||||
conf: ConfigType,
|
||||
action: Callable,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Initialize a new Trigger class and attach it."""
|
||||
|
||||
def action_payload_builder(
|
||||
extra_trigger_payload: dict[str, Any], description: str
|
||||
) -> dict[str, Any]:
|
||||
"""Build action variables."""
|
||||
payload = {
|
||||
"trigger": {
|
||||
**trigger_info["trigger_data"],
|
||||
CONF_PLATFORM: trigger_key,
|
||||
"description": description,
|
||||
**extra_trigger_payload,
|
||||
}
|
||||
}
|
||||
if CONF_VARIABLES in conf:
|
||||
trigger_variables = conf[CONF_VARIABLES]
|
||||
payload.update(trigger_variables.async_render(hass, payload))
|
||||
return payload
|
||||
|
||||
# Wrap sync action so that it is always async.
|
||||
# This simplifies the Trigger action runner interface by always returning a coroutine,
|
||||
# removing the need for integrations to check for the return type when awaiting the action.
|
||||
# This can be removed when sync actions are no longer supported.
|
||||
match get_hassjob_callable_job_type(action):
|
||||
case HassJobType.Executor:
|
||||
original_action = action
|
||||
|
||||
async def wrapped_executor_action(
|
||||
run_variables: dict[str, Any], context: Context | None = None
|
||||
) -> Any:
|
||||
"""Wrap sync action to be called in executor."""
|
||||
return await hass.async_add_executor_job(
|
||||
original_action, run_variables, context
|
||||
)
|
||||
|
||||
action = wrapped_executor_action
|
||||
|
||||
case HassJobType.Callback:
|
||||
original_action = action
|
||||
|
||||
async def wrapped_callback_action(
|
||||
run_variables: dict[str, Any], context: Context | None = None
|
||||
) -> Any:
|
||||
"""Wrap callback action to be awaitable."""
|
||||
return original_action(run_variables, context)
|
||||
|
||||
action = wrapped_callback_action
|
||||
|
||||
trigger = trigger_cls(
|
||||
hass,
|
||||
TriggerConfig(
|
||||
key=trigger_key,
|
||||
target=conf.get(CONF_TARGET),
|
||||
options=conf.get(CONF_OPTIONS),
|
||||
),
|
||||
)
|
||||
return await trigger.async_attach_action(action, action_payload_builder)
|
||||
|
||||
|
||||
async def async_initialize_triggers(
|
||||
hass: HomeAssistant,
|
||||
trigger_config: list[ConfigType],
|
||||
@@ -532,23 +662,17 @@ async def async_initialize_triggers(
|
||||
trigger_data=trigger_data,
|
||||
)
|
||||
|
||||
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
||||
if hasattr(platform, "async_get_triggers"):
|
||||
trigger_descriptors = await platform.async_get_triggers(hass)
|
||||
relative_trigger_key = get_relative_description_key(
|
||||
platform_domain, trigger_key
|
||||
)
|
||||
trigger_cls = trigger_descriptors[relative_trigger_key]
|
||||
trigger = trigger_cls(
|
||||
hass,
|
||||
TriggerConfig(
|
||||
key=trigger_key,
|
||||
target=conf.get(CONF_TARGET),
|
||||
options=conf.get(CONF_OPTIONS),
|
||||
),
|
||||
coro = _async_attach_trigger_cls(
|
||||
hass, trigger_cls, trigger_key, conf, action, info
|
||||
)
|
||||
coro = trigger.async_attach(action_wrapper, info)
|
||||
else:
|
||||
action_wrapper = _trigger_action_wrapper(hass, action, conf)
|
||||
coro = platform.async_attach_trigger(hass, conf, action_wrapper, info)
|
||||
|
||||
triggers.append(create_eager_task(coro))
|
||||
|
@@ -6,7 +6,7 @@ aiodns==3.5.0
|
||||
aiohasupervisor==0.3.3
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp==3.13.0
|
||||
aiohttp==3.12.15
|
||||
aiohttp_cors==0.8.1
|
||||
aiousbwatcher==1.1.1
|
||||
aiozoneinfo==0.2.3
|
||||
@@ -74,7 +74,7 @@ voluptuous-openapi==0.1.0
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
yarl==1.22.0
|
||||
yarl==1.20.1
|
||||
zeroconf==0.148.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
|
@@ -28,7 +28,7 @@ dependencies = [
|
||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||
# Lib can be removed with 2025.11
|
||||
"aiohasupervisor==0.3.3",
|
||||
"aiohttp==3.13.0",
|
||||
"aiohttp==3.12.15",
|
||||
"aiohttp_cors==0.8.1",
|
||||
"aiohttp-fast-zlib==0.3.0",
|
||||
"aiohttp-asyncmdnsresolver==0.1.1",
|
||||
@@ -78,7 +78,7 @@ dependencies = [
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.7.0",
|
||||
"voluptuous-openapi==0.1.0",
|
||||
"yarl==1.22.0",
|
||||
"yarl==1.20.1",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.148.0",
|
||||
]
|
||||
@@ -485,8 +485,6 @@ filterwarnings = [
|
||||
"ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources",
|
||||
|
||||
# -- tracked upstream / open PRs
|
||||
# https://github.com/kbr/fritzconnection/pull/244 - v1.15.0 - 2025-05-17
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*fritzconnection.core.soaper",
|
||||
# https://github.com/hacf-fr/meteofrance-api/pull/688 - v1.4.0 - 2025-03-26
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast",
|
||||
|
||||
@@ -524,8 +522,8 @@ filterwarnings = [
|
||||
# https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12
|
||||
# https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390
|
||||
"ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device",
|
||||
# https://pypi.org/project/pyeconet/ - v0.2.0 - 2025-10-05
|
||||
# https://github.com/w1ll1am23/pyeconet/blob/v0.2.0/src/pyeconet/api.py#L39
|
||||
# https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15
|
||||
# https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38
|
||||
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api",
|
||||
# https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15
|
||||
"ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron",
|
||||
@@ -567,6 +565,7 @@ filterwarnings = [
|
||||
# - SyntaxWarning - is with literal
|
||||
# https://github.com/majuss/lupupy/pull/15 - >0.3.2
|
||||
# https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16
|
||||
# https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16
|
||||
# https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19
|
||||
"ignore:\"is.*\" with '.*' literal:SyntaxWarning:importlib._bootstrap",
|
||||
|
||||
@@ -575,6 +574,7 @@ filterwarnings = [
|
||||
"ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition",
|
||||
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor",
|
||||
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection",
|
||||
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:plumlightpad.lightpad",
|
||||
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i",
|
||||
|
||||
# -- Websockets 14.1
|
||||
@@ -605,6 +605,8 @@ filterwarnings = [
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client",
|
||||
# https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19
|
||||
"ignore:pkg_resources is deprecated as an API:UserWarning:pilight",
|
||||
# https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery",
|
||||
# https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*ppadb",
|
||||
# https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10
|
||||
|
4
requirements.txt
generated
4
requirements.txt
generated
@@ -5,7 +5,7 @@
|
||||
# Home Assistant Core
|
||||
aiodns==3.5.0
|
||||
aiohasupervisor==0.3.3
|
||||
aiohttp==3.13.0
|
||||
aiohttp==3.12.15
|
||||
aiohttp_cors==0.8.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
@@ -50,6 +50,6 @@ uv==0.8.9
|
||||
voluptuous==0.15.2
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous-openapi==0.1.0
|
||||
yarl==1.22.0
|
||||
yarl==1.20.1
|
||||
webrtc-models==0.3.0
|
||||
zeroconf==0.148.0
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -384,7 +384,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.12.0
|
||||
aioshelly==13.11.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -366,7 +366,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.12.0
|
||||
aioshelly==13.11.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
|
@@ -3,12 +3,10 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -69,64 +67,3 @@ async def test_update_mechanism(
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes["installed_version"] == "3.1.4"
|
||||
assert state.attributes["latest_version"] == "3.1.5"
|
||||
|
||||
|
||||
async def test_update_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test update entity errors."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_ON
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = (
|
||||
AirGradientConnectionError("Boom")
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert "Unable to connect to AirGradient server to check for updates" in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert (
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_ON
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = (
|
||||
AirGradientConnectionError("Boom")
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert "Unable to connect to AirGradient server to check for updates" in caplog.text
|
||||
|
@@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, patch
|
||||
from aiocomelit.api import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, WATT
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.comelit.const import SCAN_INTERVAL
|
||||
@@ -18,20 +17,14 @@ from homeassistant.components.cover import (
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache,
|
||||
snapshot_platform,
|
||||
)
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
ENTITY_ID = "cover.cover0"
|
||||
|
||||
@@ -169,26 +162,37 @@ async def test_cover_stop_if_stopped(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cover_state",
|
||||
[
|
||||
CoverState.OPEN,
|
||||
CoverState.CLOSED,
|
||||
],
|
||||
)
|
||||
async def test_cover_restore_state(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
mock_serial_bridge_config_entry: MockConfigEntry,
|
||||
cover_state: CoverState,
|
||||
) -> None:
|
||||
"""Test cover restore state on reload."""
|
||||
|
||||
mock_restore_cache(hass, [State(ENTITY_ID, cover_state)])
|
||||
mock_serial_bridge.reset_mock()
|
||||
await setup_integration(hass, mock_serial_bridge_config_entry)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == cover_state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Open cover
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_serial_bridge.set_device_status.assert_called()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
|
||||
async def test_cover_dynamic(
|
||||
|
@@ -13,7 +13,6 @@ from homeassistant.components.application_credentials import (
|
||||
from homeassistant.components.mcp.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -101,7 +100,6 @@ def mock_config_entry_with_auth(
|
||||
"refresh_token": "test-refresh-token",
|
||||
"expires_at": config_entry_token_expiration.timestamp(),
|
||||
},
|
||||
CONF_SCOPE: ["read", "write"],
|
||||
},
|
||||
title=TEST_API_NAME,
|
||||
)
|
||||
|
@@ -11,7 +11,6 @@ import respx
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.mcp.const import (
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -43,11 +42,9 @@ OAUTH_SERVER_METADATA_RESPONSE = httpx.Response(
|
||||
{
|
||||
"authorization_endpoint": OAUTH_AUTHORIZE_URL,
|
||||
"token_endpoint": OAUTH_TOKEN_URL,
|
||||
"scopes_supported": ["read", "write"],
|
||||
}
|
||||
),
|
||||
)
|
||||
SCOPES = ["read", "write"]
|
||||
CALLBACK_PATH = "/auth/external/callback"
|
||||
OAUTH_CALLBACK_URL = f"https://example.com{CALLBACK_PATH}"
|
||||
OAUTH_CODE = "abcd"
|
||||
@@ -56,7 +53,6 @@ OAUTH_TOKEN_PAYLOAD = {
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"scope": " ".join(SCOPES),
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +295,6 @@ async def perform_oauth_flow(
|
||||
result: config_entries.ConfigFlowResult,
|
||||
authorize_url: str = OAUTH_AUTHORIZE_URL,
|
||||
token_url: str = OAUTH_TOKEN_URL,
|
||||
scopes: list[str] | None = None,
|
||||
) -> config_entries.ConfigFlowResult:
|
||||
"""Perform the common steps of the OAuth flow.
|
||||
|
||||
@@ -312,13 +307,10 @@ async def perform_oauth_flow(
|
||||
"redirect_uri": OAUTH_CALLBACK_URL,
|
||||
},
|
||||
)
|
||||
scope_param = ""
|
||||
if scopes:
|
||||
scope_param = "&scope=" + "+".join(scopes)
|
||||
assert result["url"] == (
|
||||
f"{authorize_url}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"&redirect_uri={OAUTH_CALLBACK_URL}"
|
||||
f"&state={state}{scope_param}"
|
||||
f"&state={state}"
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
@@ -335,14 +327,9 @@ async def perform_oauth_flow(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"oauth_server_metadata_response",
|
||||
"expected_authorize_url",
|
||||
"expected_token_url",
|
||||
"scopes",
|
||||
),
|
||||
("oauth_server_metadata_response", "expected_authorize_url", "expected_token_url"),
|
||||
[
|
||||
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL, SCOPES),
|
||||
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL),
|
||||
(
|
||||
httpx.Response(
|
||||
status_code=200,
|
||||
@@ -355,13 +342,11 @@ async def perform_oauth_flow(
|
||||
),
|
||||
f"{MCP_SERVER_BASE_URL}/authorize-path",
|
||||
f"{MCP_SERVER_BASE_URL}/token-path",
|
||||
None,
|
||||
),
|
||||
(
|
||||
httpx.Response(status_code=404),
|
||||
f"{MCP_SERVER_BASE_URL}/authorize",
|
||||
f"{MCP_SERVER_BASE_URL}/token",
|
||||
None,
|
||||
),
|
||||
],
|
||||
ids=(
|
||||
@@ -382,7 +367,6 @@ async def test_authentication_flow(
|
||||
oauth_server_metadata_response: httpx.Response,
|
||||
expected_authorize_url: str,
|
||||
expected_token_url: str,
|
||||
scopes: list[str] | None,
|
||||
) -> None:
|
||||
"""Test for an OAuth authentication flow for an MCP server."""
|
||||
|
||||
@@ -421,7 +405,6 @@ async def test_authentication_flow(
|
||||
result,
|
||||
authorize_url=expected_authorize_url,
|
||||
token_url=expected_token_url,
|
||||
scopes=scopes,
|
||||
)
|
||||
|
||||
# Client now accepts credentials
|
||||
@@ -440,7 +423,6 @@ async def test_authentication_flow(
|
||||
CONF_URL: MCP_SERVER_URL,
|
||||
CONF_AUTHORIZATION_URL: expected_authorize_url,
|
||||
CONF_TOKEN_URL: expected_token_url,
|
||||
CONF_SCOPE: scopes,
|
||||
}
|
||||
assert token
|
||||
token.pop("expires_at")
|
||||
@@ -554,7 +536,6 @@ async def test_authentication_flow_server_failure_abort(
|
||||
aioclient_mock,
|
||||
hass_client_no_auth,
|
||||
result,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# Client fails with an error
|
||||
@@ -610,7 +591,6 @@ async def test_authentication_flow_server_missing_tool_capabilities(
|
||||
aioclient_mock,
|
||||
hass_client_no_auth,
|
||||
result,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# Client can now authenticate
|
||||
@@ -648,9 +628,7 @@ async def test_reauth_flow(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
result = await perform_oauth_flow(
|
||||
hass, aioclient_mock, hass_client_no_auth, result, scopes=SCOPES
|
||||
)
|
||||
result = await perform_oauth_flow(hass, aioclient_mock, hass_client_no_auth, result)
|
||||
|
||||
# Verify we can connect to the server
|
||||
response = Mock()
|
||||
@@ -670,7 +648,6 @@ async def test_reauth_flow(
|
||||
CONF_URL: MCP_SERVER_URL,
|
||||
CONF_AUTHORIZATION_URL: OAUTH_AUTHORIZE_URL,
|
||||
CONF_TOKEN_URL: OAUTH_TOKEN_URL,
|
||||
CONF_SCOPE: ["read", "write"],
|
||||
}
|
||||
assert token
|
||||
token.pop("expires_at")
|
||||
|
@@ -48,10 +48,6 @@ def mock_nintendo_authenticator() -> Generator[MagicMock]:
|
||||
"homeassistant.components.nintendo_parental.config_flow.Authenticator",
|
||||
new=mock_auth_class,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.nintendo_parental.coordinator.NintendoParental.update",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
mock_auth = MagicMock()
|
||||
mock_auth._id_token = API_TOKEN
|
||||
|
@@ -94,7 +94,6 @@ async def test_service_call(
|
||||
[
|
||||
(NordPoolAuthenticationError, "authentication_error"),
|
||||
(NordPoolError, "connection_error"),
|
||||
(TimeoutError, "connection_error"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00")
|
||||
|
@@ -17,17 +17,14 @@ from pyopenweathermap import (
|
||||
from pyopenweathermap.client.owm_abstract_client import OWMClient
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.openweathermap.const import (
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.openweathermap.const import DEFAULT_LANGUAGE, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, patch
|
||||
@@ -53,6 +50,7 @@ def mock_config_entry(mode: str) -> MockConfigEntry:
|
||||
CONF_API_KEY: API_KEY,
|
||||
CONF_LATITUDE: LATITUDE,
|
||||
CONF_LONGITUDE: LONGITUDE,
|
||||
CONF_NAME: NAME,
|
||||
},
|
||||
options={
|
||||
CONF_MODE: mode,
|
||||
@@ -61,7 +59,6 @@ def mock_config_entry(mode: str) -> MockConfigEntry:
|
||||
entry_id="test",
|
||||
version=5,
|
||||
unique_id=f"{LATITUDE}-{LONGITUDE}",
|
||||
title=DEFAULT_NAME,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -41,7 +41,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'aqi',
|
||||
'friendly_name': 'OpenWeatherMap Air quality index',
|
||||
'friendly_name': 'openweathermap Air quality index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -94,7 +94,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'carbon_monoxide',
|
||||
'friendly_name': 'OpenWeatherMap Carbon monoxide',
|
||||
'friendly_name': 'openweathermap Carbon monoxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -148,7 +148,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'nitrogen_dioxide',
|
||||
'friendly_name': 'OpenWeatherMap Nitrogen dioxide',
|
||||
'friendly_name': 'openweathermap Nitrogen dioxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -202,7 +202,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'nitrogen_monoxide',
|
||||
'friendly_name': 'OpenWeatherMap Nitrogen monoxide',
|
||||
'friendly_name': 'openweathermap Nitrogen monoxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -256,7 +256,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'ozone',
|
||||
'friendly_name': 'OpenWeatherMap Ozone',
|
||||
'friendly_name': 'openweathermap Ozone',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -310,7 +310,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'pm10',
|
||||
'friendly_name': 'OpenWeatherMap PM10',
|
||||
'friendly_name': 'openweathermap PM10',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -364,7 +364,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'pm25',
|
||||
'friendly_name': 'OpenWeatherMap PM2.5',
|
||||
'friendly_name': 'openweathermap PM2.5',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -418,7 +418,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'sulphur_dioxide',
|
||||
'friendly_name': 'OpenWeatherMap Sulphur dioxide',
|
||||
'friendly_name': 'openweathermap Sulphur dioxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
@@ -471,7 +471,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Cloud coverage',
|
||||
'friendly_name': 'openweathermap Cloud coverage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
@@ -522,7 +522,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Condition',
|
||||
'friendly_name': 'openweathermap Condition',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_condition',
|
||||
@@ -577,7 +577,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Dew Point',
|
||||
'friendly_name': 'openweathermap Dew Point',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -634,7 +634,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Feels like temperature',
|
||||
'friendly_name': 'openweathermap Feels like temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -688,7 +688,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'OpenWeatherMap Humidity',
|
||||
'friendly_name': 'openweathermap Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
@@ -739,7 +739,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Precipitation kind',
|
||||
'friendly_name': 'openweathermap Precipitation kind',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_precipitation_kind',
|
||||
@@ -774,7 +774,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
|
||||
@@ -794,7 +794,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'pressure',
|
||||
'friendly_name': 'OpenWeatherMap Pressure',
|
||||
'friendly_name': 'openweathermap Pressure',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
|
||||
}),
|
||||
@@ -851,7 +851,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'precipitation_intensity',
|
||||
'friendly_name': 'OpenWeatherMap Rain',
|
||||
'friendly_name': 'openweathermap Rain',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
}),
|
||||
@@ -908,7 +908,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'precipitation_intensity',
|
||||
'friendly_name': 'OpenWeatherMap Snow',
|
||||
'friendly_name': 'openweathermap Snow',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
}),
|
||||
@@ -965,7 +965,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Temperature',
|
||||
'friendly_name': 'openweathermap Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -1018,7 +1018,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap UV Index',
|
||||
'friendly_name': 'openweathermap UV Index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'UV index',
|
||||
}),
|
||||
@@ -1055,7 +1055,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||
@@ -1075,7 +1075,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'OpenWeatherMap Visibility',
|
||||
'friendly_name': 'openweathermap Visibility',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
}),
|
||||
@@ -1126,7 +1126,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Weather',
|
||||
'friendly_name': 'openweathermap Weather',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_weather',
|
||||
@@ -1175,7 +1175,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Weather Code',
|
||||
'friendly_name': 'openweathermap Weather Code',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_weather_code',
|
||||
@@ -1227,7 +1227,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_direction',
|
||||
'friendly_name': 'OpenWeatherMap Wind bearing',
|
||||
'friendly_name': 'openweathermap Wind bearing',
|
||||
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
'unit_of_measurement': '°',
|
||||
}),
|
||||
@@ -1287,7 +1287,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'OpenWeatherMap Wind gust',
|
||||
'friendly_name': 'openweathermap Wind gust',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
@@ -1347,7 +1347,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'OpenWeatherMap Wind speed',
|
||||
'friendly_name': 'openweathermap Wind speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
@@ -1400,7 +1400,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Cloud coverage',
|
||||
'friendly_name': 'openweathermap Cloud coverage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
@@ -1451,7 +1451,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Condition',
|
||||
'friendly_name': 'openweathermap Condition',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_condition',
|
||||
@@ -1506,7 +1506,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Dew Point',
|
||||
'friendly_name': 'openweathermap Dew Point',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -1563,7 +1563,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Feels like temperature',
|
||||
'friendly_name': 'openweathermap Feels like temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -1617,7 +1617,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'OpenWeatherMap Humidity',
|
||||
'friendly_name': 'openweathermap Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
@@ -1668,7 +1668,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Precipitation kind',
|
||||
'friendly_name': 'openweathermap Precipitation kind',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_precipitation_kind',
|
||||
@@ -1703,7 +1703,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
|
||||
@@ -1723,7 +1723,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'pressure',
|
||||
'friendly_name': 'OpenWeatherMap Pressure',
|
||||
'friendly_name': 'openweathermap Pressure',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
|
||||
}),
|
||||
@@ -1780,7 +1780,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'precipitation_intensity',
|
||||
'friendly_name': 'OpenWeatherMap Rain',
|
||||
'friendly_name': 'openweathermap Rain',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
}),
|
||||
@@ -1837,7 +1837,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'precipitation_intensity',
|
||||
'friendly_name': 'OpenWeatherMap Snow',
|
||||
'friendly_name': 'openweathermap Snow',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
|
||||
}),
|
||||
@@ -1894,7 +1894,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'OpenWeatherMap Temperature',
|
||||
'friendly_name': 'openweathermap Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
@@ -1947,7 +1947,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap UV Index',
|
||||
'friendly_name': 'openweathermap UV Index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'UV index',
|
||||
}),
|
||||
@@ -1984,7 +1984,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||
@@ -2004,7 +2004,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'OpenWeatherMap Visibility',
|
||||
'friendly_name': 'openweathermap Visibility',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||
}),
|
||||
@@ -2055,7 +2055,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Weather',
|
||||
'friendly_name': 'openweathermap Weather',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_weather',
|
||||
@@ -2104,7 +2104,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'friendly_name': 'OpenWeatherMap Weather Code',
|
||||
'friendly_name': 'openweathermap Weather Code',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.openweathermap_weather_code',
|
||||
@@ -2156,7 +2156,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_direction',
|
||||
'friendly_name': 'OpenWeatherMap Wind bearing',
|
||||
'friendly_name': 'openweathermap Wind bearing',
|
||||
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
'unit_of_measurement': '°',
|
||||
}),
|
||||
@@ -2216,7 +2216,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'OpenWeatherMap Wind gust',
|
||||
'friendly_name': 'openweathermap Wind gust',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
@@ -2276,7 +2276,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'OpenWeatherMap Wind speed',
|
||||
'friendly_name': 'openweathermap Wind speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
|
@@ -65,7 +65,7 @@
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'cloud_coverage': 75,
|
||||
'dew_point': 4.0,
|
||||
'friendly_name': 'OpenWeatherMap',
|
||||
'friendly_name': 'openweathermap',
|
||||
'humidity': 82,
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure': 1000.0,
|
||||
@@ -129,7 +129,7 @@
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'cloud_coverage': 75,
|
||||
'dew_point': 4.0,
|
||||
'friendly_name': 'OpenWeatherMap',
|
||||
'friendly_name': 'openweathermap',
|
||||
'humidity': 82,
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure': 1000.0,
|
||||
@@ -194,7 +194,7 @@
|
||||
'attribution': 'Data provided by OpenWeatherMap',
|
||||
'cloud_coverage': 75,
|
||||
'dew_point': 4.0,
|
||||
'friendly_name': 'OpenWeatherMap',
|
||||
'friendly_name': 'openweathermap',
|
||||
'humidity': 82,
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure': 1000.0,
|
||||
|
@@ -7,7 +7,6 @@ import pytest
|
||||
|
||||
from homeassistant.components.openweathermap.const import (
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_OWM_MODE,
|
||||
DOMAIN,
|
||||
OWM_MODE_V30,
|
||||
@@ -17,9 +16,9 @@ from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -29,6 +28,7 @@ from .conftest import LATITUDE, LONGITUDE
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {
|
||||
CONF_NAME: "openweathermap",
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_LATITUDE: LATITUDE,
|
||||
CONF_LONGITUDE: LONGITUDE,
|
||||
@@ -36,13 +36,6 @@ CONFIG = {
|
||||
CONF_MODE: OWM_MODE_V30,
|
||||
}
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_LOCATION: {CONF_LATITUDE: LATITUDE, CONF_LONGITUDE: LONGITUDE},
|
||||
CONF_LANGUAGE: DEFAULT_LANGUAGE,
|
||||
CONF_MODE: OWM_MODE_V30,
|
||||
}
|
||||
|
||||
VALID_YAML_CONFIG = {CONF_API_KEY: "foo"}
|
||||
|
||||
|
||||
@@ -54,32 +47,31 @@ async def test_successful_config_flow(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
# create entry
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"][CONF_LATITUDE] == USER_INPUT[CONF_LOCATION][CONF_LATITUDE]
|
||||
assert result["data"][CONF_LONGITUDE] == USER_INPUT[CONF_LOCATION][CONF_LONGITUDE]
|
||||
assert result["data"][CONF_API_KEY] == USER_INPUT[CONF_API_KEY]
|
||||
|
||||
# validate entry state
|
||||
await hass.async_block_till_done()
|
||||
|
||||
conf_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entry = conf_entries[0]
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# unload entry
|
||||
await hass.config_entries.async_unload(conf_entries[0].entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == CONFIG[CONF_NAME]
|
||||
assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
|
||||
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
|
||||
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True)
|
||||
async def test_abort_config_flow(
|
||||
@@ -92,14 +84,13 @@ async def test_abort_config_flow(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], CONFIG)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
||||
|
||||
@@ -165,26 +156,19 @@ async def test_form_invalid_api_key(
|
||||
owm_client_mock: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
# invalid api key
|
||||
owm_client_mock.validate_key.return_value = False
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_api_key"}
|
||||
# valid api key
|
||||
|
||||
owm_client_mock.validate_key.return_value = True
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -193,23 +177,17 @@ async def test_form_api_call_error(
|
||||
owm_client_mock: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting up with api call error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
# simulate api call error
|
||||
owm_client_mock.validate_key.side_effect = RequestError("oops")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
# simulate successful api call
|
||||
|
||||
owm_client_mock.validate_key.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
@@ -3071,7 +3071,7 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_active_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3086,7 +3086,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_a_power',
|
||||
'entity_id': 'sensor.test_name_phase_a_active_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -3101,7 +3101,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'original_name': 'Active power',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -3111,16 +3111,16 @@
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_active_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase A Power',
|
||||
'friendly_name': 'Test name Phase A Active power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_a_power',
|
||||
'entity_id': 'sensor.test_name_phase_a_active_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -3521,7 +3521,7 @@
|
||||
'state': '227.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_active_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3536,7 +3536,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_b_power',
|
||||
'entity_id': 'sensor.test_name_phase_b_active_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -3551,7 +3551,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'original_name': 'Active power',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -3561,16 +3561,16 @@
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_active_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase B Power',
|
||||
'friendly_name': 'Test name Phase B Active power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_b_power',
|
||||
'entity_id': 'sensor.test_name_phase_b_active_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -3971,7 +3971,7 @@
|
||||
'state': '230.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_active_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3986,7 +3986,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_c_power',
|
||||
'entity_id': 'sensor.test_name_phase_c_active_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4001,7 +4001,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'original_name': 'Active power',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -4011,16 +4011,16 @@
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_active_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase C Power',
|
||||
'friendly_name': 'Test name Phase C Active power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_c_power',
|
||||
'entity_id': 'sensor.test_name_phase_c_active_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -4421,7 +4421,7 @@
|
||||
'state': '230.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4436,7 +4436,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_neutral_current',
|
||||
'entity_id': 'sensor.test_name_phase_n_current',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4451,7 +4451,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Neutral current',
|
||||
'original_name': 'Phase N current',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -4461,16 +4461,16 @@
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Test name Neutral current',
|
||||
'friendly_name': 'Test name Phase N current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_neutral_current',
|
||||
'entity_id': 'sensor.test_name_phase_n_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -4645,7 +4645,7 @@
|
||||
'state': '5415.41419',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_active_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4660,7 +4660,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_power',
|
||||
'entity_id': 'sensor.test_name_total_active_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4675,7 +4675,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'original_name': 'Total active power',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -4685,16 +4685,16 @@
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_active_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Power',
|
||||
'friendly_name': 'Test name Total active power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_power',
|
||||
'entity_id': 'sensor.test_name_total_active_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -4760,7 +4760,7 @@
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_apparent_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4775,7 +4775,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_apparent_power',
|
||||
'entity_id': 'sensor.test_name_total_apparent_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4790,7 +4790,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.APPARENT_POWER: 'apparent_power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Apparent power',
|
||||
'original_name': 'Total apparent power',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -4800,23 +4800,23 @@
|
||||
'unit_of_measurement': <UnitOfApparentPower.VOLT_AMPERE: 'VA'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_apparent_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'apparent_power',
|
||||
'friendly_name': 'Test name Apparent power',
|
||||
'friendly_name': 'Test name Total apparent power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfApparentPower.VOLT_AMPERE: 'VA'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_apparent_power',
|
||||
'entity_id': 'sensor.test_name_total_apparent_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2525.779',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_current-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4831,7 +4831,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_current',
|
||||
'entity_id': 'sensor.test_name_total_current',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4846,7 +4846,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current',
|
||||
'original_name': 'Total current',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -4856,16 +4856,16 @@
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_current-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Test name Current',
|
||||
'friendly_name': 'Test name Total current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_current',
|
||||
'entity_id': 'sensor.test_name_total_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
@@ -440,7 +440,7 @@ async def test_rpc_device_virtual_binary_sensor(
|
||||
assert state.state == STATE_ON
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-boolean:203-boolean_generic"
|
||||
assert entry.unique_id == "123456789ABC-boolean:203-boolean"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["boolean:203"], "value", False)
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -472,7 +472,7 @@ async def test_rpc_remove_virtual_binary_sensor_when_mode_toggle(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"test_name_boolean_200",
|
||||
"boolean:200-boolean_generic",
|
||||
"boolean:200-boolean",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -498,7 +498,7 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"test_name_boolean_200",
|
||||
"boolean:200-boolean_generic",
|
||||
"boolean:200-boolean",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -507,13 +507,13 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
|
||||
sub_device_entry = register_sub_device(
|
||||
device_registry,
|
||||
config_entry,
|
||||
"boolean:201-boolean_generic",
|
||||
"boolean:201-boolean",
|
||||
)
|
||||
entity_id2 = register_entity(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"boolean_201",
|
||||
"boolean:201-boolean_generic",
|
||||
"boolean:201-boolean",
|
||||
config_entry,
|
||||
device_id=sub_device_entry.id,
|
||||
)
|
||||
|
@@ -347,7 +347,7 @@ async def test_shelly_pro_3em(
|
||||
config_entry = await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
|
||||
|
||||
# Main device
|
||||
entity_id = "sensor.test_name_power"
|
||||
entity_id = "sensor.test_name_total_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -360,7 +360,7 @@ async def test_shelly_pro_3em(
|
||||
assert device_entry.name == "Test name"
|
||||
|
||||
# Phase A sub-device
|
||||
entity_id = "sensor.test_name_phase_a_power"
|
||||
entity_id = "sensor.test_name_phase_a_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -373,7 +373,7 @@ async def test_shelly_pro_3em(
|
||||
assert device_entry.name == "Test name Phase A"
|
||||
|
||||
# Phase B sub-device
|
||||
entity_id = "sensor.test_name_phase_b_power"
|
||||
entity_id = "sensor.test_name_phase_b_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -386,7 +386,7 @@ async def test_shelly_pro_3em(
|
||||
assert device_entry.name == "Test name Phase B"
|
||||
|
||||
# Phase C sub-device
|
||||
entity_id = "sensor.test_name_phase_c_power"
|
||||
entity_id = "sensor.test_name_phase_c_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -423,7 +423,7 @@ async def test_shelly_pro_3em_with_emeter_name(
|
||||
await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
|
||||
|
||||
# Main device
|
||||
entity_id = "sensor.test_name_power"
|
||||
entity_id = "sensor.test_name_total_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -436,7 +436,7 @@ async def test_shelly_pro_3em_with_emeter_name(
|
||||
assert device_entry.name == "Test name"
|
||||
|
||||
# Phase A sub-device
|
||||
entity_id = "sensor.test_name_phase_a_power"
|
||||
entity_id = "sensor.test_name_phase_a_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -449,7 +449,7 @@ async def test_shelly_pro_3em_with_emeter_name(
|
||||
assert device_entry.name == "Test name Phase A"
|
||||
|
||||
# Phase B sub-device
|
||||
entity_id = "sensor.test_name_phase_b_power"
|
||||
entity_id = "sensor.test_name_phase_b_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
@@ -462,7 +462,7 @@ async def test_shelly_pro_3em_with_emeter_name(
|
||||
assert device_entry.name == "Test name Phase B"
|
||||
|
||||
# Phase C sub-device
|
||||
entity_id = "sensor.test_name_phase_c_power"
|
||||
entity_id = "sensor.test_name_phase_c_active_power"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
|
@@ -331,7 +331,7 @@ async def test_rpc_device_virtual_number(
|
||||
assert state.attributes.get(ATTR_MODE) is mode
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-number:203-number_generic"
|
||||
assert entry.unique_id == "123456789ABC-number:203-number"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 78.9)
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -380,7 +380,7 @@ async def test_rpc_remove_virtual_number_when_mode_label(
|
||||
hass,
|
||||
NUMBER_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number_generic",
|
||||
"number:200-number",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -404,7 +404,7 @@ async def test_rpc_remove_virtual_number_when_orphaned(
|
||||
hass,
|
||||
NUMBER_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number_generic",
|
||||
"number:200-number",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
|
@@ -76,7 +76,7 @@ async def test_rpc_device_virtual_enum(
|
||||
]
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-enum:203-enum_generic"
|
||||
assert entry.unique_id == "123456789ABC-enum:203-enum"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["enum:203"], "value", "option 2")
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -128,7 +128,7 @@ async def test_rpc_remove_virtual_enum_when_mode_label(
|
||||
hass,
|
||||
SELECT_PLATFORM,
|
||||
"test_name_enum_200",
|
||||
"enum:200-enum_generic",
|
||||
"enum:200-enum",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -152,7 +152,7 @@ async def test_rpc_remove_virtual_enum_when_orphaned(
|
||||
hass,
|
||||
SELECT_PLATFORM,
|
||||
"test_name_enum_200",
|
||||
"enum:200-enum_generic",
|
||||
"enum:200-enum",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
|
@@ -1080,12 +1080,12 @@ async def test_rpc_device_virtual_text_sensor(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("old_id", "new_id", "role"),
|
||||
("old_id", "new_id", "device_class"),
|
||||
[
|
||||
("enum", "enum_generic", None),
|
||||
("enum", "enum_generic", SensorDeviceClass.ENUM),
|
||||
("number", "number_generic", None),
|
||||
("number", "number_current_humidity", "current_humidity"),
|
||||
("number", "number_current_temperature", "current_temperature"),
|
||||
("number", "number_current_humidity", SensorDeviceClass.HUMIDITY),
|
||||
("number", "number_current_temperature", SensorDeviceClass.TEMPERATURE),
|
||||
("text", "text_generic", None),
|
||||
],
|
||||
)
|
||||
@@ -1094,24 +1094,15 @@ async def test_migrate_unique_id_virtual_components_roles(
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
old_id: str,
|
||||
new_id: str,
|
||||
role: str | None,
|
||||
device_class: SensorDeviceClass | None,
|
||||
) -> None:
|
||||
"""Test migration of unique_id for virtual components to include role."""
|
||||
entry = await init_integration(hass, 3, skip_setup=True)
|
||||
unique_base = f"{MOCK_MAC}-{old_id}:200"
|
||||
old_unique_id = f"{unique_base}-{old_id}"
|
||||
new_unique_id = f"{unique_base}-{new_id}"
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
if role:
|
||||
config[f"{old_id}:200"] = {
|
||||
"role": role,
|
||||
}
|
||||
else:
|
||||
config[f"{old_id}:200"] = {}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
suggested_object_id="test_name_test_sensor",
|
||||
@@ -1120,6 +1111,7 @@ async def test_migrate_unique_id_virtual_components_roles(
|
||||
platform=DOMAIN,
|
||||
unique_id=old_unique_id,
|
||||
config_entry=entry,
|
||||
original_device_class=device_class,
|
||||
)
|
||||
assert entity.unique_id == old_unique_id
|
||||
|
||||
|
@@ -645,7 +645,7 @@ async def test_rpc_device_virtual_switch(
|
||||
assert state.state == STATE_ON
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-boolean:200-boolean_generic"
|
||||
assert entry.unique_id == "123456789ABC-boolean:200-boolean"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", False)
|
||||
await hass.services.async_call(
|
||||
@@ -715,7 +715,7 @@ async def test_rpc_remove_virtual_switch_when_mode_label(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"test_name_boolean_200",
|
||||
"boolean:200-boolean_generic",
|
||||
"boolean:200-boolean",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -741,7 +741,7 @@ async def test_rpc_remove_virtual_switch_when_orphaned(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"test_name_boolean_200",
|
||||
"boolean:200-boolean_generic",
|
||||
"boolean:200-boolean",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -750,13 +750,13 @@ async def test_rpc_remove_virtual_switch_when_orphaned(
|
||||
sub_device_entry = register_sub_device(
|
||||
device_registry,
|
||||
config_entry,
|
||||
"boolean:201-boolean_generic",
|
||||
"boolean:201-boolean",
|
||||
)
|
||||
entity_id2 = register_entity(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"boolean_201",
|
||||
"boolean:201-boolean_generic",
|
||||
"boolean:201-boolean",
|
||||
config_entry,
|
||||
device_id=sub_device_entry.id,
|
||||
)
|
||||
|
@@ -62,7 +62,7 @@ async def test_rpc_device_virtual_text(
|
||||
assert state.state == "lorem ipsum"
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-text:203-text_generic"
|
||||
assert entry.unique_id == "123456789ABC-text:203-text"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["text:203"], "value", "dolor sit amet")
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -107,7 +107,7 @@ async def test_rpc_remove_virtual_text_when_mode_label(
|
||||
hass,
|
||||
TEXT_PLATFORM,
|
||||
"test_name_text_200",
|
||||
"text:200-text_generic",
|
||||
"text:200-text",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -131,7 +131,7 @@ async def test_rpc_remove_virtual_text_when_orphaned(
|
||||
hass,
|
||||
TEXT_PLATFORM,
|
||||
"test_name_text_200",
|
||||
"text:200-text_generic",
|
||||
"text:200-text",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
|
@@ -97,7 +97,6 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
"aq_sensor_3_ikea",
|
||||
"aeotec_ms6",
|
||||
"da_ac_airsensor_01001",
|
||||
"da_ac_rac_000001",
|
||||
"da_ac_rac_000003",
|
||||
@@ -157,7 +156,6 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
||||
"heatit_ztrm3_thermostat",
|
||||
"heatit_zpushwall",
|
||||
"generic_ef00_v1",
|
||||
"gas_detector",
|
||||
"bosch_radiator_thermostat_ii",
|
||||
"im_speaker_ai_0001",
|
||||
"im_smarttag2_ble_uwb",
|
||||
|
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"components": {
|
||||
"main": {
|
||||
"ultravioletIndex": {
|
||||
"ultravioletIndex": {
|
||||
"value": 0,
|
||||
"timestamp": "2025-09-30T15:13:46.521Z"
|
||||
}
|
||||
},
|
||||
"relativeHumidityMeasurement": {
|
||||
"humidity": {
|
||||
"value": 60.0,
|
||||
"unit": "%",
|
||||
"timestamp": "2025-09-30T15:13:45.441Z"
|
||||
}
|
||||
},
|
||||
"temperatureMeasurement": {
|
||||
"temperatureRange": {
|
||||
"value": null
|
||||
},
|
||||
"temperature": {
|
||||
"value": 22.2,
|
||||
"unit": "C",
|
||||
"timestamp": "2025-09-30T16:13:50.478Z"
|
||||
}
|
||||
},
|
||||
"refresh": {},
|
||||
"motionSensor": {
|
||||
"motion": {
|
||||
"value": "inactive",
|
||||
"timestamp": "2025-09-30T15:33:27.594Z"
|
||||
}
|
||||
},
|
||||
"illuminanceMeasurement": {
|
||||
"illuminance": {
|
||||
"value": 30,
|
||||
"unit": "lux",
|
||||
"timestamp": "2025-09-30T15:13:52.607Z"
|
||||
}
|
||||
},
|
||||
"battery": {
|
||||
"quantity": {
|
||||
"value": null
|
||||
},
|
||||
"battery": {
|
||||
"value": 100,
|
||||
"unit": "%",
|
||||
"timestamp": "2025-09-30T15:13:46.166Z"
|
||||
},
|
||||
"type": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"tamperAlert": {
|
||||
"tamper": {
|
||||
"value": "clear",
|
||||
"timestamp": "2025-09-30T14:06:07.064Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"components": {
|
||||
"main": {
|
||||
"momentary": {},
|
||||
"gasDetector": {
|
||||
"gas": {
|
||||
"value": "clear",
|
||||
"timestamp": "2025-10-02T03:18:27.139Z"
|
||||
}
|
||||
},
|
||||
"signalStrength": {
|
||||
"rssi": {
|
||||
"value": -71,
|
||||
"unit": "dBm",
|
||||
"timestamp": "2025-10-07T04:17:08.419Z"
|
||||
},
|
||||
"lqi": {
|
||||
"value": 148,
|
||||
"timestamp": "2025-10-07T04:32:08.512Z"
|
||||
}
|
||||
},
|
||||
"refresh": {}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "00f9233e-fdaa-4020-99d4-e0073e53996a",
|
||||
"name": "aeotec-ms6",
|
||||
"label": "Parent's Bedroom Sensor",
|
||||
"manufacturerName": "SmartThingsCommunity",
|
||||
"presentationId": "6d160aa8-7f54-3611-b7de-0b335d162529",
|
||||
"deviceManufacturerCode": "0086-0102-0064",
|
||||
"locationId": "3478ae40-8bd4-40b8-b7e6-f25e3cf86409",
|
||||
"ownerId": "fe7f9079-8e23-8307-fb7e-4d58929391cf",
|
||||
"roomId": "f1bb7871-3a3d-48da-b23f-0e1297e8acb0",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "main",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "motionSensor",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "temperatureMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "relativeHumidityMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "illuminanceMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "ultravioletIndex",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "tamperAlert",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "battery",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "MotionSensor",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"createTime": "2025-04-17T05:47:05.803Z",
|
||||
"parentDeviceId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
|
||||
"profile": {
|
||||
"id": "9893d370-2af6-32a0-86c5-f1a6d2b9fea7"
|
||||
},
|
||||
"zwave": {
|
||||
"networkId": "BE",
|
||||
"driverId": "42930682-019d-4dbe-8098-760d7afb3c7f",
|
||||
"executingLocally": true,
|
||||
"hubId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
|
||||
"networkSecurityLevel": "ZWAVE_LEGACY_NON_SECURE",
|
||||
"provisioningState": "PROVISIONED",
|
||||
"manufacturerId": 134,
|
||||
"productType": 258,
|
||||
"productId": 100,
|
||||
"fingerprintType": "ZWAVE_MANUFACTURER",
|
||||
"fingerprintId": "Aeotec/MS6/US"
|
||||
},
|
||||
"type": "ZWAVE",
|
||||
"restrictionTier": 0,
|
||||
"allowed": null,
|
||||
"executionContext": "LOCAL",
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "d830b46f-f094-4560-b8c3-7690032fdb4c",
|
||||
"name": "generic-ef00-v1",
|
||||
"label": "Gas Detector",
|
||||
"manufacturerName": "SmartThingsCommunity",
|
||||
"presentationId": "d4b88195-fd5b-39d3-ac6f-7070655f08ab",
|
||||
"deviceManufacturerCode": "_TZE284_chbyv06x",
|
||||
"locationId": "7139bb09-31e3-4fad-bf03-b9ad02e57b41",
|
||||
"ownerId": "00126705-d35b-27ee-d18b-17620d9929e7",
|
||||
"roomId": "5adccb3a-8ae7-41c0-bc58-7ba80ff78a18",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "Detector",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "gasDetector",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "momentary",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "signalStrength",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Siren",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"createTime": "2025-05-25T04:55:42.440Z",
|
||||
"profile": {
|
||||
"id": "1d34dd9d-6840-3df6-a6d0-5d9f4a4af2e1"
|
||||
},
|
||||
"zigbee": {
|
||||
"eui": "A4C138C524A5BC8D",
|
||||
"networkId": "1575",
|
||||
"driverId": "bc7fd1bc-eb00-4b7f-8977-172acf823508",
|
||||
"executingLocally": true,
|
||||
"hubId": "0afe704f-eabb-4e4d-8333-6c73903e4f84",
|
||||
"provisioningState": "DRIVER_SWITCH",
|
||||
"fingerprintType": "ZIGBEE_GENERIC",
|
||||
"fingerprintId": "GenericEF00"
|
||||
},
|
||||
"type": "ZIGBEE",
|
||||
"restrictionTier": 0,
|
||||
"allowed": null,
|
||||
"executionContext": "LOCAL",
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
@@ -1,102 +1,4 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.MOTION: 'motion'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Motion',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_motionSensor_motion_motion',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'motion',
|
||||
'friendly_name': "Parent's Bedroom Sensor Motion",
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.TAMPER: 'tamper'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Tamper',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_tamperAlert_tamper_tamper',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'tamper',
|
||||
'friendly_name': "Parent's Bedroom Sensor Tamper",
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[c2c_arlo_pro_3_switch][binary_sensor.2nd_floor_hallway_motion-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -2670,55 +2572,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.gas_detector_gas',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_gasDetector_gas_gas',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gas Detector Gas',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.gas_detector_gas',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[iphone][binary_sensor.iphone_presence-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@@ -64,37 +64,6 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[aeotec_ms6]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': 'https://account.smartthings.com',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'smartthings',
|
||||
'00f9233e-fdaa-4020-99d4-e0073e53996a',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': "Parent's Bedroom Sensor",
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[aq_sensor_3_ikea]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
@@ -1335,37 +1304,6 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[gas_detector]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': 'https://account.smartthings.com',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'smartthings',
|
||||
'd830b46f-f094-4560-b8c3-7690032fdb4c',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': 'Gas Detector',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[gas_meter]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
@@ -163,269 +163,6 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_battery_battery_battery',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': "Parent's Bedroom Sensor Battery",
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_relativeHumidityMeasurement_humidity_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': "Parent's Bedroom Sensor Humidity",
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '60.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ILLUMINANCE: 'illuminance'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Illuminance',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_illuminanceMeasurement_illuminance_illuminance',
|
||||
'unit_of_measurement': 'lx',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'illuminance',
|
||||
'friendly_name': "Parent's Bedroom Sensor Illuminance",
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'lx',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_temperatureMeasurement_temperature_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': "Parent's Bedroom Sensor Temperature",
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '22.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'UV index',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'uv_index',
|
||||
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_ultravioletIndex_ultravioletIndex_ultravioletIndex',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': "Parent's Bedroom Sensor UV index",
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -12914,110 +12651,6 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.gas_detector_link_quality',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Link quality',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'link_quality',
|
||||
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_lqi_lqi',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Gas Detector Link quality',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gas_detector_link_quality',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '148',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.gas_detector_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Signal strength',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_rssi_rssi',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': 'Gas Detector Signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gas_detector_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-71',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_meter][sensor.gas_meter_gas-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@@ -24,9 +24,7 @@ from homeassistant.helpers.trigger import (
|
||||
DATA_PLUGGABLE_ACTIONS,
|
||||
PluggableAction,
|
||||
Trigger,
|
||||
TriggerActionType,
|
||||
TriggerConfig,
|
||||
TriggerInfo,
|
||||
TriggerActionRunner,
|
||||
_async_get_trigger_platform,
|
||||
async_initialize_triggers,
|
||||
async_validate_trigger_config,
|
||||
@@ -449,7 +447,31 @@ async def test_pluggable_action(
|
||||
assert not plug_2
|
||||
|
||||
|
||||
async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
|
||||
class TriggerActionFunctionTypeHelper:
|
||||
"""Helper for testing different trigger action function types."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Init helper."""
|
||||
self.action_calls = []
|
||||
|
||||
@callback
|
||||
def cb_action(self, *args):
|
||||
"""Callback action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
def sync_action(self, *args):
|
||||
"""Sync action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
async def async_action(self, *args):
|
||||
"""Async action."""
|
||||
self.action_calls.append([*args])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action_method", ["cb_action", "sync_action", "async_action"])
|
||||
async def test_platform_multiple_triggers(
|
||||
hass: HomeAssistant, action_method: str
|
||||
) -> None:
|
||||
"""Test a trigger platform with multiple trigger."""
|
||||
|
||||
class MockTrigger(Trigger):
|
||||
@@ -462,30 +484,23 @@ async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
|
||||
"""Validate config."""
|
||||
return config
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
|
||||
class MockTrigger1(MockTrigger):
|
||||
"""Mock trigger 1."""
|
||||
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
action({"trigger": "test_trigger_1"})
|
||||
run_action({"extra": "test_trigger_1"}, "trigger 1 desc")
|
||||
|
||||
class MockTrigger2(MockTrigger):
|
||||
"""Mock trigger 2."""
|
||||
|
||||
async def async_attach(
|
||||
self,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
action({"trigger": "test_trigger_2"})
|
||||
run_action({"extra": "test_trigger_2"}, "trigger 2 desc")
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
return {
|
||||
@@ -508,22 +523,41 @@ async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
|
||||
|
||||
log_cb = MagicMock()
|
||||
|
||||
action_calls = []
|
||||
action_helper = TriggerActionFunctionTypeHelper()
|
||||
action_method = getattr(action_helper, action_method)
|
||||
|
||||
@callback
|
||||
def cb_action(*args):
|
||||
action_calls.append([*args])
|
||||
await async_initialize_triggers(hass, config_1, action_method, "test", "", log_cb)
|
||||
assert len(action_helper.action_calls) == 1
|
||||
assert action_helper.action_calls[0][0] == {
|
||||
"trigger": {
|
||||
"alias": None,
|
||||
"description": "trigger 1 desc",
|
||||
"extra": "test_trigger_1",
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "test",
|
||||
}
|
||||
}
|
||||
action_helper.action_calls.clear()
|
||||
|
||||
await async_initialize_triggers(hass, config_1, cb_action, "test", "", log_cb)
|
||||
assert action_calls == [[{"trigger": "test_trigger_1"}]]
|
||||
action_calls.clear()
|
||||
|
||||
await async_initialize_triggers(hass, config_2, cb_action, "test", "", log_cb)
|
||||
assert action_calls == [[{"trigger": "test_trigger_2"}]]
|
||||
action_calls.clear()
|
||||
await async_initialize_triggers(hass, config_2, action_method, "test", "", log_cb)
|
||||
assert len(action_helper.action_calls) == 1
|
||||
assert action_helper.action_calls[0][0] == {
|
||||
"trigger": {
|
||||
"alias": None,
|
||||
"description": "trigger 2 desc",
|
||||
"extra": "test_trigger_2",
|
||||
"id": "0",
|
||||
"idx": "0",
|
||||
"platform": "test.trig_2",
|
||||
}
|
||||
}
|
||||
action_helper.action_calls.clear()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
await async_initialize_triggers(hass, config_3, cb_action, "test", "", log_cb)
|
||||
await async_initialize_triggers(
|
||||
hass, config_3, action_method, "test", "", log_cb
|
||||
)
|
||||
|
||||
|
||||
async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
|
||||
|
@@ -8,11 +8,13 @@ from unittest.mock import Mock, patch
|
||||
import pytest
|
||||
|
||||
from homeassistant import const
|
||||
from homeassistant.components import alarm_control_panel
|
||||
|
||||
from .common import (
|
||||
extract_stack_to_frame,
|
||||
help_test_all,
|
||||
import_and_test_deprecated_constant,
|
||||
import_and_test_deprecated_constant_enum,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +52,30 @@ def test_deprecated_constant_name_changes(
|
||||
)
|
||||
|
||||
|
||||
def _create_tuples_alarm_states(
|
||||
enum: type[Enum], constant_prefix: str, remove_in_version: str
|
||||
) -> list[tuple[Enum, str]]:
|
||||
return [(enum_field, constant_prefix, remove_in_version) for enum_field in enum]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enum", "constant_prefix", "remove_in_version"),
|
||||
_create_tuples_alarm_states(
|
||||
alarm_control_panel.AlarmControlPanelState, "STATE_ALARM_", "2025.11"
|
||||
),
|
||||
)
|
||||
def test_deprecated_constants_alarm(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
enum: Enum,
|
||||
constant_prefix: str,
|
||||
remove_in_version: str,
|
||||
) -> None:
|
||||
"""Test deprecated constants."""
|
||||
import_and_test_deprecated_constant_enum(
|
||||
caplog, const, enum, constant_prefix, remove_in_version
|
||||
)
|
||||
|
||||
|
||||
def test_deprecated_unit_of_conductivity_alias() -> None:
|
||||
"""Test UnitOfConductivity deprecation."""
|
||||
|
||||
|
Reference in New Issue
Block a user