mirror of
https://github.com/home-assistant/core.git
synced 2025-10-13 05:39:37 +00:00
Compare commits
29 Commits
remove-cod
...
water_hier
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03b58a4c21 | ||
![]() |
53d1bbb530 | ||
![]() |
a3ef55274e | ||
![]() |
2034915457 | ||
![]() |
9e46d7964a | ||
![]() |
f9828a227b | ||
![]() |
3341fa5f33 | ||
![]() |
e38ae47e76 | ||
![]() |
934c0e3c4c | ||
![]() |
994a6ae7ed | ||
![]() |
cdbe93c289 | ||
![]() |
56f90e4d96 | ||
![]() |
34977abfec | ||
![]() |
5622103eb1 | ||
![]() |
b9a1ab4a44 | ||
![]() |
18997833c4 | ||
![]() |
f99b194afc | ||
![]() |
566a347da7 | ||
![]() |
881306f6a4 | ||
![]() |
f63504af01 | ||
![]() |
d140b82a70 | ||
![]() |
681211b1a5 | ||
![]() |
6c8b1f3618 | ||
![]() |
d341065c34 | ||
![]() |
81b1346080 | ||
![]() |
5613be3980 | ||
![]() |
fbcf0eb94c | ||
![]() |
1c7b9cc354 | ||
![]() |
75e900606e |
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"cloud_ceiling": {
|
||||
"default": "mdi:weather-fog"
|
||||
},
|
||||
@@ -34,9 +37,6 @@
|
||||
"thunderstorm_probability_night": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"translation_key": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"tree_pollen": {
|
||||
"default": "mdi:tree-outline"
|
||||
},
|
||||
|
@@ -1,7 +1,9 @@
|
||||
"""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
|
||||
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -31,6 +34,7 @@ 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."""
|
||||
@@ -47,10 +51,27 @@ 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."""
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
try:
|
||||
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,6 +15,7 @@ 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
|
||||
@@ -29,23 +30,19 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
known_devices: set[int] = set()
|
||||
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)
|
||||
|
||||
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))
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
|
||||
|
||||
class ComelitVedoBinarySensorEntity(
|
||||
|
@@ -7,14 +7,21 @@ 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 CoverDeviceClass, CoverEntity
|
||||
from homeassistant.components.cover import (
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
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 bridge_api_call
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -29,21 +36,19 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
known_devices: set[int] = set()
|
||||
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)
|
||||
|
||||
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))
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, COVER)
|
||||
)
|
||||
|
||||
|
||||
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@@ -62,7 +67,6 @@ 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."""
|
||||
@@ -98,7 +102,6 @@ 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()
|
||||
@@ -124,5 +127,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if last_state := await self.async_get_last_state():
|
||||
self._last_state = last_state.state
|
||||
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
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -27,21 +27,19 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
known_devices: set[int] = set()
|
||||
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)
|
||||
|
||||
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))
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, LIGHT)
|
||||
)
|
||||
|
||||
|
||||
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):
|
||||
|
@@ -20,6 +20,7 @@ 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
|
||||
@@ -65,24 +66,22 @@ async def async_setup_bridge_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
known_devices: set[int] = set()
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, OTHER)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_vedo_entry(
|
||||
@@ -94,24 +93,22 @@ async def async_setup_vedo_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
known_devices: set[int] = set()
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
|
||||
)
|
||||
|
||||
|
||||
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 bridge_api_call
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -28,35 +28,20 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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))
|
||||
for dev_type in (IRRIGATION, OTHER):
|
||||
config_entry.async_on_unload(
|
||||
new_device_listener(coordinator, _add_new_entities, dev_type)
|
||||
)
|
||||
|
||||
|
||||
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
||||
|
@@ -4,7 +4,11 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.api import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
@@ -19,8 +23,11 @@ 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."""
|
||||
@@ -113,3 +120,41 @@ 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,13 +45,18 @@ 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, callback
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
@@ -192,7 +197,7 @@ class IntentCache:
|
||||
|
||||
|
||||
async def async_setup_default_agent(
|
||||
hass: core.HomeAssistant,
|
||||
hass: HomeAssistant,
|
||||
entity_component: EntityComponent[ConversationEntity],
|
||||
config_intents: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -201,15 +206,13 @@ async def async_setup_default_agent(
|
||||
await entity_component.async_add_entities([agent])
|
||||
await get_agent_manager(hass).async_setup_default_agent(agent)
|
||||
|
||||
@core.callback
|
||||
def async_entity_state_listener(
|
||||
event: core.Event[core.EventStateChangedData],
|
||||
) -> None:
|
||||
@callback
|
||||
def async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
|
||||
"""Set expose flag on new entities."""
|
||||
async_should_expose(hass, DOMAIN, event.data["entity_id"])
|
||||
|
||||
@core.callback
|
||||
def async_hass_started(hass: core.HomeAssistant) -> None:
|
||||
@callback
|
||||
def async_hass_started(hass: HomeAssistant) -> None:
|
||||
"""Set expose flag on all entities."""
|
||||
for state in hass.states.async_all():
|
||||
async_should_expose(hass, DOMAIN, state.entity_id)
|
||||
@@ -224,9 +227,7 @@ class DefaultAgent(ConversationEntity):
|
||||
_attr_name = "Home Assistant"
|
||||
_attr_supported_features = ConversationEntityFeature.CONTROL
|
||||
|
||||
def __init__(
|
||||
self, hass: core.HomeAssistant, config_intents: dict[str, Any]
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_intents: dict[str, Any]) -> None:
|
||||
"""Initialize the default agent."""
|
||||
self.hass = hass
|
||||
self._lang_intents: dict[str, LanguageIntents | object] = {}
|
||||
@@ -259,7 +260,7 @@ class DefaultAgent(ConversationEntity):
|
||||
"""Return a list of supported languages."""
|
||||
return get_languages()
|
||||
|
||||
@core.callback
|
||||
@callback
|
||||
def _filter_entity_registry_changes(
|
||||
self, event_data: er.EventEntityRegistryUpdatedData
|
||||
) -> bool:
|
||||
@@ -268,12 +269,12 @@ class DefaultAgent(ConversationEntity):
|
||||
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
|
||||
)
|
||||
|
||||
@core.callback
|
||||
def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool:
|
||||
@callback
|
||||
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
|
||||
"""Filter state changed events."""
|
||||
return not event_data["old_state"] or not event_data["new_state"]
|
||||
|
||||
@core.callback
|
||||
@callback
|
||||
def _listen_clear_slot_list(self) -> None:
|
||||
"""Listen for changes that can invalidate slot list."""
|
||||
assert self._unsub_clear_slot_list is None
|
||||
@@ -890,7 +891,7 @@ class DefaultAgent(ConversationEntity):
|
||||
) -> str:
|
||||
# Get first matched or unmatched state.
|
||||
# This is available in the response template as "state".
|
||||
state1: core.State | None = None
|
||||
state1: State | None = None
|
||||
if intent_response.matched_states:
|
||||
state1 = intent_response.matched_states[0]
|
||||
elif intent_response.unmatched_states:
|
||||
@@ -1589,7 +1590,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
|
||||
|
||||
def _get_match_error_response(
|
||||
hass: core.HomeAssistant,
|
||||
hass: HomeAssistant,
|
||||
match_error: intent.MatchFailedError,
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when target matching fails."""
|
||||
|
@@ -116,6 +116,10 @@ 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,16 +41,12 @@ 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=async_update_data,
|
||||
function=callback(lambda: self.async_set_updated_data(self._height)),
|
||||
)
|
||||
|
||||
async def async_connect(self) -> bool:
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -23,7 +24,13 @@ 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_TOKEN_URL, DOMAIN
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import TokenManager, mcp_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -41,9 +48,17 @@ 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
|
||||
) -> AuthorizationServer:
|
||||
) -> OAuthConfig:
|
||||
"""Discover the OAuth configuration for the MCP server.
|
||||
|
||||
This implements the functionality in the MCP spec for discovery. If the MCP server URL
|
||||
@@ -65,9 +80,11 @@ 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 AuthorizationServer(
|
||||
authorize_url=str(parsed_url.with_path("/authorize")),
|
||||
token_url=str(parsed_url.with_path("/token")),
|
||||
return OAuthConfig(
|
||||
authorization_server=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:
|
||||
@@ -81,9 +98,15 @@ 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))
|
||||
return AuthorizationServer(
|
||||
authorize_url=authorize_url,
|
||||
token_url=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,
|
||||
)
|
||||
|
||||
|
||||
@@ -130,6 +153,7 @@ 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
|
||||
@@ -170,7 +194,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
to find the OAuth medata then run the OAuth authentication flow.
|
||||
"""
|
||||
try:
|
||||
authorization_server = await async_discover_oauth_config(
|
||||
oauth_config = await async_discover_oauth_config(
|
||||
self.hass, self.data[CONF_URL]
|
||||
)
|
||||
except TimeoutConnectError:
|
||||
@@ -181,11 +205,13 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
else:
|
||||
_LOGGER.info("OAuth configuration: %s", authorization_server)
|
||||
_LOGGER.info("OAuth configuration: %s", oauth_config)
|
||||
self.oauth_config = oauth_config
|
||||
self.data.update(
|
||||
{
|
||||
CONF_AUTHORIZATION_URL: authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: authorization_server.token_url,
|
||||
CONF_AUTHORIZATION_URL: oauth_config.authorization_server.authorize_url,
|
||||
CONF_TOKEN_URL: oauth_config.authorization_server.token_url,
|
||||
CONF_SCOPE: oauth_config.scopes,
|
||||
}
|
||||
)
|
||||
return await self.async_step_credentials_choice()
|
||||
@@ -197,6 +223,15 @@ 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,3 +5,4 @@ DOMAIN = "mcp"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_AUTHORIZATION_URL = "authorization_url"
|
||||
CONF_TOKEN_URL = "token_url"
|
||||
CONF_SCOPE = "scope"
|
||||
|
@@ -54,6 +54,7 @@ _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 as error:
|
||||
except (NordPoolError, TimeoutError) as error:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
|
@@ -31,38 +31,39 @@ async def async_setup_entry(
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
entities = {
|
||||
event.uid: ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(
|
||||
ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
|
||||
)
|
||||
|
||||
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 entities:
|
||||
entities[entry.unique_id] = ONVIFBinarySensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
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))
|
||||
|
||||
async_add_entities(entities.values())
|
||||
async_add_entities(entities)
|
||||
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(entities)):
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("binary_sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities: dict[str, ONVIFBinarySensor] = {
|
||||
uid: ONVIFBinarySensor(uid, device, name=entity_names[uid])
|
||||
for uid in missing
|
||||
}
|
||||
new_entities = [
|
||||
ONVIFBinarySensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
if new_entities:
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
|
||||
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)
|
||||
|
||||
entities = {
|
||||
event.uid: ONVIFSensor(event.uid, device, name=entity_names[event.uid])
|
||||
for event in events
|
||||
}
|
||||
uids = set()
|
||||
entities = []
|
||||
for event in events:
|
||||
uids.add(event.uid)
|
||||
entities.append(ONVIFSensor(event.uid, device, name=entity_names[event.uid]))
|
||||
|
||||
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 entities:
|
||||
entities[entry.unique_id] = ONVIFSensor(
|
||||
entry.unique_id, device, entry=entry
|
||||
)
|
||||
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))
|
||||
|
||||
async_add_entities(entities.values())
|
||||
async_add_entities(entities)
|
||||
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(entities)):
|
||||
if not (missing := uids_by_platform.difference(uids)):
|
||||
return
|
||||
|
||||
events = device.events.get_platform("sensor")
|
||||
entity_names = build_event_entity_names(events)
|
||||
|
||||
new_entities: dict[str, ONVIFSensor] = {
|
||||
uid: ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
}
|
||||
new_entities = [
|
||||
ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
|
||||
]
|
||||
if new_entities:
|
||||
entities.update(new_entities)
|
||||
async_add_entities(new_entities.values())
|
||||
uids.update(missing)
|
||||
async_add_entities(new_entities)
|
||||
|
||||
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, CONF_NAME
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS
|
||||
@@ -25,7 +25,6 @@ type OpenweathermapConfigEntry = ConfigEntry[OpenweathermapData]
|
||||
class OpenweathermapData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
name: str
|
||||
mode: str
|
||||
coordinator: OWMUpdateCoordinator
|
||||
|
||||
@@ -34,7 +33,6 @@ 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]
|
||||
@@ -51,7 +49,7 @@ async def async_setup_entry(
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator)
|
||||
entry.runtime_data = OpenweathermapData(mode, owm_coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@@ -14,12 +14,17 @@ 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 import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
LanguageSelector,
|
||||
LanguageSelectorConfig,
|
||||
LocationSelector,
|
||||
LocationSelectorConfig,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONFIG_FLOW_VERSION,
|
||||
@@ -34,10 +39,12 @@ from .utils import build_data_and_options, validate_api_key
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
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_LOCATION): LocationSelector(
|
||||
LocationSelectorConfig(radius=False)
|
||||
),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
@@ -45,7 +52,9 @@ USER_SCHEMA = vol.Schema(
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
|
||||
),
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
|
||||
}
|
||||
)
|
||||
@@ -70,8 +79,8 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
latitude = user_input[CONF_LOCATION][CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LOCATION][CONF_LONGITUDE]
|
||||
mode = user_input[CONF_MODE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
@@ -82,15 +91,21 @@ 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=user_input[CONF_NAME], data=data, options=options
|
||||
title=DEFAULT_NAME, data=data, options=options
|
||||
)
|
||||
schema_data = user_input
|
||||
else:
|
||||
schema_data = {
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
},
|
||||
CONF_LANGUAGE: self.hass.config.language,
|
||||
}
|
||||
|
||||
|
@@ -121,6 +121,7 @@ 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,
|
||||
@@ -158,6 +159,7 @@ 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,
|
||||
@@ -227,7 +229,6 @@ 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
|
||||
@@ -242,7 +243,6 @@ async def async_setup_entry(
|
||||
elif domain_data.mode == OWM_MODE_AIRPOLLUTION:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -252,7 +252,6 @@ async def async_setup_entry(
|
||||
else:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
unique_id,
|
||||
description,
|
||||
coordinator,
|
||||
@@ -270,7 +269,6 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: OWMUpdateCoordinator,
|
||||
@@ -284,7 +282,6 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -12,16 +12,14 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"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",
|
||||
"latitude": "Latitude of the location",
|
||||
"longitude": "Longitude of the location",
|
||||
"location": "Location to get the weather data for",
|
||||
"mode": "Mode for the OpenWeatherMap API",
|
||||
"name": "Name for this OpenWeatherMap location"
|
||||
},
|
||||
|
@@ -57,14 +57,13 @@ 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(name, unique_id, mode, weather_coordinator)
|
||||
owm_weather = OpenWeatherMapWeather(unique_id, mode, weather_coordinator)
|
||||
|
||||
async_add_entities([owm_weather], False)
|
||||
|
||||
@@ -93,7 +92,6 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
mode: str,
|
||||
weather_coordinator: OWMUpdateCoordinator,
|
||||
@@ -105,7 +103,6 @@ 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 = 30.0
|
||||
CONNECT_TIMEOUT = 60.0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"config_subentries": {
|
||||
"partition": {
|
||||
"entry_type": "Partition",
|
||||
"initiate_flow": {
|
||||
"user": "Add partition"
|
||||
},
|
||||
@@ -57,6 +58,7 @@
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"entry_type": "Zone",
|
||||
"initiate_flow": {
|
||||
"user": "Add zone"
|
||||
},
|
||||
@@ -91,6 +93,7 @@
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"entry_type": "Output",
|
||||
"initiate_flow": {
|
||||
"user": "Add output"
|
||||
},
|
||||
@@ -125,6 +128,7 @@
|
||||
}
|
||||
},
|
||||
"switchable_output": {
|
||||
"entry_type": "Switchable output",
|
||||
"initiate_flow": {
|
||||
"user": "Add switchable output"
|
||||
},
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from aioshelly.ble.const import BLE_SCRIPT_NAME
|
||||
@@ -63,6 +64,7 @@ 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,
|
||||
@@ -323,6 +325,12 @@ 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
|
||||
from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE
|
||||
from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -270,12 +270,21 @@ RPC_SENSORS: Final = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"boolean": RpcBinarySensorDescription(
|
||||
"boolean_generic": 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,3 +308,5 @@ 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,6 +29,7 @@ from .utils import (
|
||||
get_rpc_device_info,
|
||||
get_rpc_entity_name,
|
||||
get_rpc_key_instances,
|
||||
get_rpc_role_by_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -189,9 +190,9 @@ def async_setup_rpc_attribute_entities(
|
||||
if description.models and coordinator.model not in description.models:
|
||||
continue
|
||||
|
||||
if description.role and description.role != coordinator.device.config[
|
||||
key
|
||||
].get("role", "generic"):
|
||||
if description.role and description.role != get_rpc_role_by_key(
|
||||
coordinator.device.config, key
|
||||
):
|
||||
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.11.0"],
|
||||
"requirements": ["aioshelly==13.12.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@@ -24,7 +24,16 @@ 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, VIRTUAL_NUMBER_MODE_MAP
|
||||
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 .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -183,7 +192,7 @@ RPC_NUMBERS: Final = {
|
||||
method="blu_trv_set_external_temperature",
|
||||
entity_class=RpcBluTrvExtTempNumber,
|
||||
),
|
||||
"number": RpcNumberDescription(
|
||||
"number_generic": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -197,6 +206,58 @@ 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,12 +38,13 @@ class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
|
||||
|
||||
|
||||
RPC_SELECT_ENTITIES: Final = {
|
||||
"enum": RpcSelectDescription(
|
||||
"enum_generic": RpcSelectDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SELECT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -3,8 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Final, cast
|
||||
from typing import Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
@@ -37,13 +36,12 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import HomeAssistant
|
||||
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, LOGGER
|
||||
from .const import CONF_SLEEP_PERIOD
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -551,7 +549,7 @@ RPC_SENSORS: Final = {
|
||||
"a_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="a_act_power",
|
||||
name="Active power",
|
||||
name="Power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -561,7 +559,7 @@ RPC_SENSORS: Final = {
|
||||
"b_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="b_act_power",
|
||||
name="Active power",
|
||||
name="Power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -571,7 +569,7 @@ RPC_SENSORS: Final = {
|
||||
"c_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="c_act_power",
|
||||
name="Active power",
|
||||
name="Power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -581,7 +579,7 @@ RPC_SENSORS: Final = {
|
||||
"total_act_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_act_power",
|
||||
name="Total active power",
|
||||
name="Power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -627,7 +625,7 @@ RPC_SENSORS: Final = {
|
||||
"total_aprt_power": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_aprt_power",
|
||||
name="Total apparent power",
|
||||
name="Apparent power",
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -882,7 +880,7 @@ RPC_SENSORS: Final = {
|
||||
"n_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="n_current",
|
||||
name="Phase N current",
|
||||
name="Neutral current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -892,7 +890,7 @@ RPC_SENSORS: Final = {
|
||||
"total_current": RpcSensorDescription(
|
||||
key="em",
|
||||
sub_key="total_current",
|
||||
name="Total current",
|
||||
name="Current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -1384,7 +1382,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement="pulse",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value=lambda status, _: status["total"],
|
||||
removal_condition=lambda config, _status, key: (
|
||||
removal_condition=lambda config, _, key: (
|
||||
config[key]["type"] != "count" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
@@ -1424,7 +1422,7 @@ RPC_SENSORS: Final = {
|
||||
"text_generic": RpcSensorDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
@@ -1432,7 +1430,7 @@ RPC_SENSORS: Final = {
|
||||
"number_generic": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
unit=get_virtual_component_unit,
|
||||
@@ -1441,7 +1439,7 @@ RPC_SENSORS: Final = {
|
||||
"enum_generic": RpcSensorDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
removal_condition=lambda config, _, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
options_fn=lambda config: config["options"],
|
||||
@@ -1456,7 +1454,7 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
removal_condition=lambda config, _status, key: config[key].get("enable", False)
|
||||
removal_condition=lambda config, _, key: config[key].get("enable", False)
|
||||
is False,
|
||||
entity_class=RpcBluTrvSensor,
|
||||
),
|
||||
@@ -1606,7 +1604,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_act_energy": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Total Active Energy",
|
||||
name="Energy",
|
||||
value=lambda status, _: float(status["total_act_energy"]),
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
@@ -1618,7 +1616,7 @@ RPC_SENSORS: Final = {
|
||||
"object_total_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
name="Total Power",
|
||||
name="Power",
|
||||
value=lambda status, _: float(status["total_power"]),
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
@@ -1663,39 +1661,6 @@ 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,
|
||||
@@ -1715,12 +1680,6 @@ 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,6 +21,13 @@ 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,
|
||||
@@ -87,7 +94,7 @@ RPC_RELAY_SWITCHES = {
|
||||
}
|
||||
|
||||
RPC_SWITCHES = {
|
||||
"boolean": RpcSwitchDescription(
|
||||
"boolean_generic": RpcSwitchDescription(
|
||||
key="boolean",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -97,6 +104,120 @@ 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,12 +38,13 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription):
|
||||
|
||||
|
||||
RPC_TEXT_ENTITIES: Final = {
|
||||
"text": RpcTextDescription(
|
||||
"text_generic": RpcTextDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, TEXT_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
}
|
||||
|
||||
|
@@ -484,6 +484,11 @@ 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])
|
||||
@@ -934,3 +939,35 @@ 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,6 +179,13 @@ 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,7 +530,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ILLUMINANCE_MEASUREMENT: {
|
||||
Attribute.ILLUMINANCE: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -842,7 +841,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.SIGNAL_STRENGTH: {
|
||||
Attribute.LQI: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -1001,7 +999,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
],
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.TVOC_MEASUREMENT: {
|
||||
Attribute.TVOC_LEVEL: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
@@ -1012,7 +1009,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
)
|
||||
]
|
||||
},
|
||||
# Haven't seen at devices yet
|
||||
Capability.ULTRAVIOLET_INDEX: {
|
||||
Attribute.ULTRAVIOLET_INDEX: [
|
||||
SmartThingsSensorEntityDescription(
|
||||
|
@@ -20,8 +20,9 @@ set_temperature:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
max: 250
|
||||
step: 0.5
|
||||
mode: box
|
||||
unit_of_measurement: "°"
|
||||
operation_mode:
|
||||
example: eco
|
||||
|
@@ -134,7 +134,6 @@ 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,
|
||||
|
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Final
|
||||
|
||||
from .generated.entity_platforms import EntityPlatforms
|
||||
from .helpers.deprecation import (
|
||||
DeprecatedConstant,
|
||||
DeprecatedConstantEnum,
|
||||
EnumWithDeprecatedMembers,
|
||||
all_with_deprecated_constants,
|
||||
@@ -316,60 +315,6 @@ 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"
|
||||
|
@@ -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.12.15
|
||||
aiohttp==3.13.0
|
||||
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.20.1
|
||||
yarl==1.22.0
|
||||
zeroconf==0.148.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
|
@@ -5,6 +5,9 @@ build_from:
|
||||
armhf: "ghcr.io/home-assistant/armhf-homeassistant:"
|
||||
amd64: "ghcr.io/home-assistant/amd64-homeassistant:"
|
||||
i386: "ghcr.io/home-assistant/i386-homeassistant:"
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
cosign:
|
||||
base_identity: https://github.com/home-assistant/core/.*
|
||||
identity: https://github.com/home-assistant/core/.*
|
||||
|
@@ -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.12.15",
|
||||
"aiohttp==3.13.0",
|
||||
"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.20.1",
|
||||
"yarl==1.22.0",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.148.0",
|
||||
]
|
||||
@@ -485,6 +485,8 @@ 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",
|
||||
|
||||
@@ -522,8 +524,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.1.28 - 2025-02-15
|
||||
# https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38
|
||||
# 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
|
||||
"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",
|
||||
@@ -565,7 +567,6 @@ 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",
|
||||
|
||||
@@ -574,7 +575,6 @@ 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,8 +605,6 @@ 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.12.15
|
||||
aiohttp==3.13.0
|
||||
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.20.1
|
||||
yarl==1.22.0
|
||||
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.11.0
|
||||
aioshelly==13.12.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.11.0
|
||||
aioshelly==13.12.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
|
@@ -3,10 +3,12 @@
|
||||
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, Platform
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -67,3 +69,64 @@ 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,6 +5,7 @@ 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
|
||||
@@ -17,14 +18,20 @@ 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
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
ENTITY_ID = "cover.cover0"
|
||||
|
||||
@@ -162,37 +169,26 @@ 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_serial_bridge.reset_mock()
|
||||
mock_restore_cache(hass, [State(ENTITY_ID, cover_state)])
|
||||
await setup_integration(hass, mock_serial_bridge_config_entry)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
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
|
||||
assert state.state == cover_state
|
||||
|
||||
|
||||
async def test_cover_dynamic(
|
||||
|
@@ -13,6 +13,7 @@ from homeassistant.components.application_credentials import (
|
||||
from homeassistant.components.mcp.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -100,6 +101,7 @@ 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,6 +11,7 @@ import respx
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.mcp.const import (
|
||||
CONF_AUTHORIZATION_URL,
|
||||
CONF_SCOPE,
|
||||
CONF_TOKEN_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -42,9 +43,11 @@ 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"
|
||||
@@ -53,6 +56,7 @@ OAUTH_TOKEN_PAYLOAD = {
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"scope": " ".join(SCOPES),
|
||||
}
|
||||
|
||||
|
||||
@@ -295,6 +299,7 @@ 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.
|
||||
|
||||
@@ -307,10 +312,13 @@ 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}"
|
||||
f"&state={state}{scope_param}"
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
@@ -327,9 +335,14 @@ async def perform_oauth_flow(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("oauth_server_metadata_response", "expected_authorize_url", "expected_token_url"),
|
||||
(
|
||||
"oauth_server_metadata_response",
|
||||
"expected_authorize_url",
|
||||
"expected_token_url",
|
||||
"scopes",
|
||||
),
|
||||
[
|
||||
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL),
|
||||
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL, SCOPES),
|
||||
(
|
||||
httpx.Response(
|
||||
status_code=200,
|
||||
@@ -342,11 +355,13 @@ 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=(
|
||||
@@ -367,6 +382,7 @@ 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."""
|
||||
|
||||
@@ -405,6 +421,7 @@ async def test_authentication_flow(
|
||||
result,
|
||||
authorize_url=expected_authorize_url,
|
||||
token_url=expected_token_url,
|
||||
scopes=scopes,
|
||||
)
|
||||
|
||||
# Client now accepts credentials
|
||||
@@ -423,6 +440,7 @@ 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")
|
||||
@@ -536,6 +554,7 @@ async def test_authentication_flow_server_failure_abort(
|
||||
aioclient_mock,
|
||||
hass_client_no_auth,
|
||||
result,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# Client fails with an error
|
||||
@@ -591,6 +610,7 @@ async def test_authentication_flow_server_missing_tool_capabilities(
|
||||
aioclient_mock,
|
||||
hass_client_no_auth,
|
||||
result,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# Client can now authenticate
|
||||
@@ -628,7 +648,9 @@ 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)
|
||||
result = await perform_oauth_flow(
|
||||
hass, aioclient_mock, hass_client_no_auth, result, scopes=SCOPES
|
||||
)
|
||||
|
||||
# Verify we can connect to the server
|
||||
response = Mock()
|
||||
@@ -648,6 +670,7 @@ 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,6 +48,10 @@ 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,6 +94,7 @@ 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,14 +17,17 @@ from pyopenweathermap import (
|
||||
from pyopenweathermap.client.owm_abstract_client import OWMClient
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.openweathermap.const import DEFAULT_LANGUAGE, DOMAIN
|
||||
from homeassistant.components.openweathermap.const import (
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, patch
|
||||
@@ -50,7 +53,6 @@ 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,
|
||||
@@ -59,6 +61,7 @@ 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': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'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': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'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': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'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': 2,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'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,6 +7,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.openweathermap.const import (
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_OWM_MODE,
|
||||
DOMAIN,
|
||||
OWM_MODE_V30,
|
||||
@@ -16,9 +17,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
|
||||
@@ -28,7 +29,6 @@ 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,6 +36,13 @@ 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"}
|
||||
|
||||
|
||||
@@ -47,31 +54,32 @@ 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"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
# create entry
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
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
|
||||
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(
|
||||
@@ -84,13 +92,14 @@ 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"], CONFIG)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
||||
|
||||
@@ -156,19 +165,26 @@ async def test_form_invalid_api_key(
|
||||
owm_client_mock: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
owm_client_mock.validate_key.return_value = False
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
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,
|
||||
)
|
||||
|
||||
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=CONFIG
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -177,17 +193,23 @@ async def test_form_api_call_error(
|
||||
owm_client_mock: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting up with api call error."""
|
||||
owm_client_mock.validate_key.side_effect = RequestError("oops")
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
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,
|
||||
)
|
||||
|
||||
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=CONFIG
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
@@ -3071,7 +3071,7 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_active_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3086,7 +3086,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_a_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_a_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': 'Active power',
|
||||
'original_name': '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_active_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase A Active power',
|
||||
'friendly_name': 'Test name Phase A Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_a_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_a_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_active_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3536,7 +3536,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_b_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_b_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': 'Active power',
|
||||
'original_name': '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_active_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase B Active power',
|
||||
'friendly_name': 'Test name Phase B Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_b_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_b_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_active_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -3986,7 +3986,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_c_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_c_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': 'Active power',
|
||||
'original_name': '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_active_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Phase C Active power',
|
||||
'friendly_name': 'Test name Phase C Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_c_active_power',
|
||||
'entity_id': 'sensor.test_name_phase_c_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_phase_n_current-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4436,7 +4436,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_phase_n_current',
|
||||
'entity_id': 'sensor.test_name_neutral_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': 'Phase N current',
|
||||
'original_name': 'Neutral 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_phase_n_current-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Test name Phase N current',
|
||||
'friendly_name': 'Test name Neutral current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_phase_n_current',
|
||||
'entity_id': 'sensor.test_name_neutral_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_total_active_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4660,7 +4660,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_total_active_power',
|
||||
'entity_id': 'sensor.test_name_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': 'Total active power',
|
||||
'original_name': '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_total_active_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Test name Total active power',
|
||||
'friendly_name': 'Test name Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_total_active_power',
|
||||
'entity_id': 'sensor.test_name_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_total_apparent_power-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4775,7 +4775,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_total_apparent_power',
|
||||
'entity_id': 'sensor.test_name_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': 'Total apparent power',
|
||||
'original_name': '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_total_apparent_power-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'apparent_power',
|
||||
'friendly_name': 'Test name Total apparent power',
|
||||
'friendly_name': 'Test name Apparent power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfApparentPower.VOLT_AMPERE: 'VA'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_total_apparent_power',
|
||||
'entity_id': 'sensor.test_name_apparent_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2525.779',
|
||||
})
|
||||
# ---
|
||||
# name: test_shelly_pro_3em[sensor.test_name_total_current-entry]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4831,7 +4831,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_name_total_current',
|
||||
'entity_id': 'sensor.test_name_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': 'Total current',
|
||||
'original_name': '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_total_current-state]
|
||||
# name: test_shelly_pro_3em[sensor.test_name_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Test name Total current',
|
||||
'friendly_name': 'Test name Current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_name_total_current',
|
||||
'entity_id': 'sensor.test_name_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"
|
||||
assert entry.unique_id == "123456789ABC-boolean:203-boolean_generic"
|
||||
|
||||
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",
|
||||
"boolean:200-boolean_generic",
|
||||
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",
|
||||
"boolean:200-boolean_generic",
|
||||
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",
|
||||
"boolean:201-boolean_generic",
|
||||
)
|
||||
entity_id2 = register_entity(
|
||||
hass,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"boolean_201",
|
||||
"boolean:201-boolean",
|
||||
"boolean:201-boolean_generic",
|
||||
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_total_active_power"
|
||||
entity_id = "sensor.test_name_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_a_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_b_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_c_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_total_active_power"
|
||||
entity_id = "sensor.test_name_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_a_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_b_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_active_power"
|
||||
entity_id = "sensor.test_name_phase_c_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"
|
||||
assert entry.unique_id == "123456789ABC-number:203-number_generic"
|
||||
|
||||
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",
|
||||
"number:200-number_generic",
|
||||
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",
|
||||
"number:200-number_generic",
|
||||
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"
|
||||
assert entry.unique_id == "123456789ABC-enum:203-enum_generic"
|
||||
|
||||
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",
|
||||
"enum:200-enum_generic",
|
||||
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",
|
||||
"enum:200-enum_generic",
|
||||
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", "device_class"),
|
||||
("old_id", "new_id", "role"),
|
||||
[
|
||||
("enum", "enum_generic", SensorDeviceClass.ENUM),
|
||||
("enum", "enum_generic", None),
|
||||
("number", "number_generic", None),
|
||||
("number", "number_current_humidity", SensorDeviceClass.HUMIDITY),
|
||||
("number", "number_current_temperature", SensorDeviceClass.TEMPERATURE),
|
||||
("number", "number_current_humidity", "current_humidity"),
|
||||
("number", "number_current_temperature", "current_temperature"),
|
||||
("text", "text_generic", None),
|
||||
],
|
||||
)
|
||||
@@ -1094,15 +1094,24 @@ 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,
|
||||
device_class: SensorDeviceClass | None,
|
||||
role: str | 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",
|
||||
@@ -1111,7 +1120,6 @@ 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"
|
||||
assert entry.unique_id == "123456789ABC-boolean:200-boolean_generic"
|
||||
|
||||
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",
|
||||
"boolean:200-boolean_generic",
|
||||
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",
|
||||
"boolean:200-boolean_generic",
|
||||
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",
|
||||
"boolean:201-boolean_generic",
|
||||
)
|
||||
entity_id2 = register_entity(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"boolean_201",
|
||||
"boolean:201-boolean",
|
||||
"boolean:201-boolean_generic",
|
||||
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"
|
||||
assert entry.unique_id == "123456789ABC-text:203-text_generic"
|
||||
|
||||
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",
|
||||
"text:200-text_generic",
|
||||
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",
|
||||
"text:200-text_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
|
@@ -97,6 +97,7 @@ 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",
|
||||
@@ -156,6 +157,7 @@ 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",
|
||||
|
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"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,4 +1,102 @@
|
||||
# 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({
|
||||
@@ -2572,6 +2670,55 @@
|
||||
'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,6 +64,37 @@
|
||||
'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,
|
||||
@@ -1304,6 +1335,37 @@
|
||||
'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,6 +163,269 @@
|
||||
'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({
|
||||
@@ -12651,6 +12914,110 @@
|
||||
'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({
|
||||
|
@@ -8,13 +8,11 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,30 +50,6 @@ 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