mirror of
https://github.com/home-assistant/core.git
synced 2026-01-13 18:48:45 +00:00
Compare commits
38 Commits
add-includ
...
condition_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26874335ca | ||
|
|
a6221d16b6 | ||
|
|
51701cab7c | ||
|
|
010e1f2d0d | ||
|
|
66909fc9ca | ||
|
|
90a28c95c8 | ||
|
|
83f2c53e8c | ||
|
|
514b6e243c | ||
|
|
742230c7be | ||
|
|
acb6b1444e | ||
|
|
f358b2231a | ||
|
|
fd24cffa6b | ||
|
|
0b5d6ee538 | ||
|
|
d125bb88d1 | ||
|
|
2ab51f582a | ||
|
|
f9b32811b2 | ||
|
|
41a423e140 | ||
|
|
f717867657 | ||
|
|
ab202a03db | ||
|
|
46a3e5e5b5 | ||
|
|
0163a4d289 | ||
|
|
6c1bf31a3c | ||
|
|
a434760a80 | ||
|
|
798990fadc | ||
|
|
b3d9d92e4a | ||
|
|
1082a9ca69 | ||
|
|
c247f56658 | ||
|
|
e7f71781f1 | ||
|
|
c4b2c5e621 | ||
|
|
7779609a76 | ||
|
|
7b9a5f897c | ||
|
|
6eccbfc1cf | ||
|
|
0da518e951 | ||
|
|
e5851b7920 | ||
|
|
1b9364e8b5 | ||
|
|
8460d4f5e2 | ||
|
|
8fd35cd70d | ||
|
|
88be115699 |
@@ -40,7 +40,8 @@
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
|
||||
4
.vscode/settings.default.jsonc
vendored
4
.vscode/settings.default.jsonc
vendored
@@ -7,8 +7,8 @@
|
||||
"python.testing.pytestEnabled": false,
|
||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright is too pedantic for Home Assistant
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
},
|
||||
|
||||
@@ -85,6 +85,22 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
value = system[key]
|
||||
return value
|
||||
|
||||
async def _async_update_sys_params(self, params: dict[str, Any]) -> None:
|
||||
"""Send system parameters to API."""
|
||||
_params = {
|
||||
API_SYSTEM_ID: self.system_id,
|
||||
**params,
|
||||
}
|
||||
_LOGGER.debug("update_sys_params=%s", _params)
|
||||
try:
|
||||
await self.coordinator.airzone.set_sys_parameters(_params)
|
||||
except AirzoneError as error:
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set system {self.entity_id}: {error}"
|
||||
) from error
|
||||
|
||||
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
|
||||
|
||||
|
||||
class AirzoneHotWaterEntity(AirzoneEntity):
|
||||
"""Define an Airzone Hot Water entity."""
|
||||
|
||||
@@ -20,6 +20,7 @@ from aioairzone.const import (
|
||||
AZD_MODES,
|
||||
AZD_Q_ADAPT,
|
||||
AZD_SLEEP,
|
||||
AZD_SYSTEMS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -85,14 +86,7 @@ def main_zone_options(
|
||||
return [k for k, v in options.items() if v in modes]
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
key=AZD_MODE,
|
||||
options_dict=MODE_DICT,
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_Q_ADAPT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -104,6 +98,17 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
)
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
key=AZD_MODE,
|
||||
options_dict=MODE_DICT,
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_COLD_ANGLE,
|
||||
@@ -140,16 +145,37 @@ async def async_setup_entry(
|
||||
"""Add Airzone select from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_systems: set[str] = set()
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of select."""
|
||||
|
||||
entities: list[AirzoneBaseSelect] = []
|
||||
|
||||
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
|
||||
received_systems = set(systems_data)
|
||||
new_systems = received_systems - added_systems
|
||||
if new_systems:
|
||||
entities.extend(
|
||||
AirzoneSystemSelect(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_id,
|
||||
systems_data.get(system_id),
|
||||
)
|
||||
for system_id in new_systems
|
||||
for description in SYSTEM_SELECT_TYPES
|
||||
if description.key in systems_data.get(system_id)
|
||||
)
|
||||
added_systems.update(new_systems)
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
entities: list[AirzoneZoneSelect] = [
|
||||
entities.extend(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -161,8 +187,8 @@ async def async_setup_entry(
|
||||
for description in MAIN_ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
|
||||
]
|
||||
entities += [
|
||||
)
|
||||
entities.extend(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -173,10 +199,11 @@ async def async_setup_entry(
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
]
|
||||
async_add_entities(entities)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
@@ -203,6 +230,38 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||
self._attr_current_option = self._get_current_option()
|
||||
|
||||
|
||||
class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone System select."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneSelectDescription,
|
||||
entry: ConfigEntry,
|
||||
system_id: str,
|
||||
system_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_data)
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_options = self.entity_description.options_fn(
|
||||
system_data, description.options_dict
|
||||
)
|
||||
|
||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
param = self.entity_description.api_param
|
||||
value = self.entity_description.options_dict[option]
|
||||
await self._async_update_sys_params({param: value})
|
||||
|
||||
|
||||
class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone Zone select."""
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import dateutil
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -179,6 +181,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
LAST_S_TEST: SensorEntityDescription(
|
||||
key=LAST_S_TEST,
|
||||
translation_key="last_self_test",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
"lastxfer": SensorEntityDescription(
|
||||
key="lastxfer",
|
||||
@@ -232,6 +235,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"masterupd": SensorEntityDescription(
|
||||
key="masterupd",
|
||||
translation_key="master_update",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"maxlinev": SensorEntityDescription(
|
||||
@@ -365,6 +369,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"starttime": SensorEntityDescription(
|
||||
key="starttime",
|
||||
translation_key="startup_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"statflag": SensorEntityDescription(
|
||||
@@ -416,16 +421,19 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"xoffbat": SensorEntityDescription(
|
||||
key="xoffbat",
|
||||
translation_key="transfer_from_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbatt": SensorEntityDescription(
|
||||
key="xoffbatt",
|
||||
translation_key="transfer_from_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xonbatt": SensorEntityDescription(
|
||||
key="xonbatt",
|
||||
translation_key="transfer_to_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
@@ -529,7 +537,13 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||
data = self.coordinator.data[key]
|
||||
|
||||
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(data)
|
||||
if not self.native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = inferred_unit
|
||||
|
||||
|
||||
@@ -111,11 +111,17 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
return None
|
||||
return self.coordinator.data.state.target_temperature.value
|
||||
|
||||
@property
|
||||
def _hvac_mode_value(self) -> int | str | None:
|
||||
"""Return the raw hvac_mode value from the coordinator."""
|
||||
if (hvac_mode := self.coordinator.data.state.hvac_mode) is None:
|
||||
return None
|
||||
return hvac_mode.value
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
|
||||
if hvac_mode_value is None:
|
||||
if (hvac_mode_value := self._hvac_mode_value) is None:
|
||||
return None
|
||||
# BSB-Lan returns integer values: 0=off, 1=auto, 2=eco, 3=heat
|
||||
if isinstance(hvac_mode_value, int):
|
||||
@@ -125,9 +131,8 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
|
||||
# BSB-Lan mode 2 is eco/reduced mode
|
||||
if hvac_mode_value == 2:
|
||||
if self._hvac_mode_value == 2:
|
||||
return PRESET_ECO
|
||||
return PRESET_NONE
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
||||
name=data.device.name,
|
||||
manufacturer="BSBLAN Inc.",
|
||||
model=data.info.device_identification.value,
|
||||
model=(
|
||||
data.info.device_identification.value
|
||||
if data.info.device_identification
|
||||
else None
|
||||
),
|
||||
sw_version=data.device.version,
|
||||
configuration_url=f"http://{host}",
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==3.1.6"],
|
||||
"requirements": ["python-bsblan==4.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -50,7 +50,6 @@ from . import (
|
||||
from .client import CloudClient
|
||||
from .const import (
|
||||
CONF_ACCOUNT_LINK_SERVER,
|
||||
CONF_ACCOUNTS_SERVER,
|
||||
CONF_ACME_SERVER,
|
||||
CONF_ALEXA,
|
||||
CONF_ALIASES,
|
||||
@@ -138,7 +137,6 @@ _BASE_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
|
||||
vol.Optional(CONF_ACCOUNTS_SERVER): str,
|
||||
vol.Optional(CONF_ACME_SERVER): str,
|
||||
vol.Optional(CONF_API_SERVER): str,
|
||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||
|
||||
@@ -76,7 +76,6 @@ CONF_GOOGLE_ACTIONS = "google_actions"
|
||||
CONF_USER_POOL_ID = "user_pool_id"
|
||||
|
||||
CONF_ACCOUNT_LINK_SERVER = "account_link_server"
|
||||
CONF_ACCOUNTS_SERVER = "accounts_server"
|
||||
CONF_ACME_SERVER = "acme_server"
|
||||
CONF_API_SERVER = "api_server"
|
||||
CONF_DISCOVERY_SERVICE_ACTIONS = "discovery_service_actions"
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.7.0"],
|
||||
"requirements": ["hass-nabucasa==1.9.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
|
||||
key="sleep/timeInBed",
|
||||
translation_key="sleep_time_in_bed",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
icon="mdi:hotel",
|
||||
icon="mdi:bed",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
scope=FitbitScope.SLEEP,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
||||
@@ -164,13 +164,12 @@ def _async_wol_buttons_list(
|
||||
class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity):
|
||||
"""Defines a FRITZ!Box Tools Wake On LAN button."""
|
||||
|
||||
_attr_icon = "mdi:lan-pending"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_translation_key = "wake_on_lan"
|
||||
|
||||
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
||||
"""Initialize Fritz!Box WOL button."""
|
||||
super().__init__(avm_wrapper, device)
|
||||
self._name = f"{self.hostname} Wake on LAN"
|
||||
self._attr_unique_id = f"{self._mac}_wake_on_lan"
|
||||
self._is_available = True
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEFAULT_DEVICE_NAME
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .entity import FritzDeviceBase
|
||||
from .helpers import device_filter_out_from_trackers
|
||||
@@ -71,6 +72,7 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity):
|
||||
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
||||
"""Initialize a FRITZ!Box device."""
|
||||
super().__init__(avm_wrapper, device)
|
||||
self._attr_name: str = device.hostname or DEFAULT_DEVICE_NAME
|
||||
self._last_activity: datetime.datetime | None = device.last_activity
|
||||
|
||||
@property
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_DEVICE_NAME, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AvmWrapper
|
||||
from .models import FritzDevice
|
||||
|
||||
@@ -21,21 +21,17 @@ from .models import FritzDevice
|
||||
class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):
|
||||
"""Entity base class for a device connected to a FRITZ!Box device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
||||
"""Initialize a FRITZ!Box device."""
|
||||
super().__init__(avm_wrapper)
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._mac: str = device.mac_address
|
||||
self._name: str = device.hostname or DEFAULT_DEVICE_NAME
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)}
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Return the primary ip address of the device."""
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"button": {
|
||||
"cleanup": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"wake_on_lan": {
|
||||
"default": "mdi:lan-pending"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -48,6 +51,11 @@
|
||||
"max_kb_s_sent": {
|
||||
"default": "mdi:upload"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"internet_access": {
|
||||
"default": "mdi:router-wireless-settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["fritzconnection"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==1.0.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -13,9 +13,7 @@ rules:
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name:
|
||||
status: todo
|
||||
comment: partially done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
},
|
||||
"reconnect": {
|
||||
"name": "Reconnect"
|
||||
},
|
||||
"wake_on_lan": {
|
||||
"name": "Wake on LAN"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -162,6 +165,11 @@
|
||||
"max_kb_s_sent": {
|
||||
"name": "Max connection upload throughput"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"internet_access": {
|
||||
"name": "Internet access"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -499,13 +499,12 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Defines a FRITZ!Box Tools DeviceProfile switch."""
|
||||
|
||||
_attr_icon = "mdi:router-wireless-settings"
|
||||
_attr_translation_key = "internet_access"
|
||||
|
||||
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
||||
"""Init Fritz profile."""
|
||||
super().__init__(avm_wrapper, device)
|
||||
self._attr_is_on: bool = False
|
||||
self._name = f"{device.hostname} Internet Access"
|
||||
self._attr_unique_id = f"{self._mac}_internet_access"
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260107.0"]
|
||||
"requirements": ["home-assistant-frontend==20260107.1"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ammonia": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"benzene": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"nitrogen_dioxide": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"nitrogen_monoxide": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"non_methane_hydrocarbons": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"ozone": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
|
||||
@@ -99,6 +99,14 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
"local_aqi": data.indexes[1].display_name
|
||||
},
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="c6h6",
|
||||
translation_key="benzene",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.c6h6.concentration.units,
|
||||
value_fn=lambda x: x.pollutants.c6h6.concentration.value,
|
||||
exists_fn=lambda x: "c6h6" in {p.code for p in x.pollutants},
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="co",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -106,6 +114,30 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
|
||||
value_fn=lambda x: x.pollutants.co.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="nh3",
|
||||
translation_key="ammonia",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.nh3.concentration.units,
|
||||
value_fn=lambda x: x.pollutants.nh3.concentration.value,
|
||||
exists_fn=lambda x: "nh3" in {p.code for p in x.pollutants},
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="nmhc",
|
||||
translation_key="non_methane_hydrocarbons",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.nmhc.concentration.units,
|
||||
value_fn=lambda x: x.pollutants.nmhc.concentration.value,
|
||||
exists_fn=lambda x: "nmhc" in {p.code for p in x.pollutants},
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="no",
|
||||
translation_key="nitrogen_monoxide",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.no.concentration.units,
|
||||
value_fn=lambda x: x.pollutants.no.concentration.value,
|
||||
exists_fn=lambda x: "no" in {p.code for p in x.pollutants},
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="no2",
|
||||
translation_key="nitrogen_dioxide",
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ammonia": {
|
||||
"name": "Ammonia"
|
||||
},
|
||||
"benzene": {
|
||||
"name": "Benzene"
|
||||
},
|
||||
"local_aqi": {
|
||||
"name": "{local_aqi} AQI"
|
||||
},
|
||||
@@ -189,6 +195,9 @@
|
||||
"name": "{local_aqi} dominant pollutant",
|
||||
"state": {
|
||||
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"nh3": "[%key:component::google_air_quality::entity::sensor::ammonia::name%]",
|
||||
"nmhc": "[%key:component::google_air_quality::entity::sensor::non_methane_hydrocarbons::name%]",
|
||||
"no": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||
"no2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
@@ -199,6 +208,12 @@
|
||||
"nitrogen_dioxide": {
|
||||
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
|
||||
},
|
||||
"nitrogen_monoxide": {
|
||||
"name": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]"
|
||||
},
|
||||
"non_methane_hydrocarbons": {
|
||||
"name": "Non-methane hydrocarbons"
|
||||
},
|
||||
"ozone": {
|
||||
"name": "[%key:component::sensor::entity_component::ozone::name%]"
|
||||
},
|
||||
|
||||
21
homeassistant/components/hdfury/diagnostics.py
Normal file
21
homeassistant/components/hdfury/diagnostics.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Diagnostics for HDFury Integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import HDFuryCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HDFuryCoordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"board": coordinator.data.board,
|
||||
"info": coordinator.data.info,
|
||||
"config": coordinator.data.config,
|
||||
}
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyhik.constants import SENSOR_MAP
|
||||
from pyhik.hikvision import HikCamera
|
||||
import requests
|
||||
|
||||
@@ -70,13 +71,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
device_type=device_type,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Device %s (type=%s) initial event_states: %s",
|
||||
device_name,
|
||||
device_type,
|
||||
camera.current_event_states,
|
||||
)
|
||||
|
||||
# For NVRs or devices with no detected events, try to fetch events from ISAPI
|
||||
# Use broader notification methods for NVRs since they often use 'record' etc.
|
||||
if device_type == "NVR" or not camera.current_event_states:
|
||||
nvr_notification_methods = {"center", "HTTP", "record", "email", "beep"}
|
||||
|
||||
def fetch_and_inject_nvr_events() -> None:
|
||||
"""Fetch and inject NVR events in a single executor job."""
|
||||
if nvr_events := camera.get_event_triggers():
|
||||
camera.inject_events(nvr_events)
|
||||
nvr_events = camera.get_event_triggers(nvr_notification_methods)
|
||||
_LOGGER.debug("NVR events fetched with extended methods: %s", nvr_events)
|
||||
if nvr_events:
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
|
||||
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==10.0.0"],
|
||||
"requirements": ["python-homewizard-energy==10.0.1"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -256,6 +256,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
@@ -289,6 +291,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -149,6 +149,8 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -66,8 +66,9 @@ class MatterRangeNumberEntityDescription(
|
||||
format_max_value: Callable[[float], float] = lambda x: x
|
||||
|
||||
# command: a custom callback to create the command to send to the device
|
||||
# the callback's argument will be the index of the selected list value
|
||||
command: Callable[[int], ClusterCommand]
|
||||
# the callback's argument will be the converted device value from ha_to_device
|
||||
# if omitted the command will just be a write_attribute command to the primary attribute
|
||||
command: Callable[[int], ClusterCommand] | None = None
|
||||
|
||||
|
||||
class MatterNumber(MatterEntity, NumberEntity):
|
||||
@@ -99,9 +100,15 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
send_value = self.entity_description.ha_to_device(value)
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
if self.entity_description.command:
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
)
|
||||
return
|
||||
# regular write attribute to set the new value
|
||||
await self.write_attribute(
|
||||
value=send_value,
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -253,6 +260,30 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(custom_clusters.EveCluster.Attributes.Altitude,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterRangeNumberEntityDescription(
|
||||
key="ThermostatOccupiedSetback",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="occupied_setback",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
format_min_value=lambda x: x / 10,
|
||||
format_max_value=lambda x: x / 10,
|
||||
min_attribute=clusters.Thermostat.Attributes.OccupiedSetbackMin,
|
||||
max_attribute=clusters.Thermostat.Attributes.OccupiedSetbackMax,
|
||||
native_step=0.5,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterRangeNumber,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.OccupiedSetback,
|
||||
clusters.Thermostat.Attributes.OccupiedSetbackMin,
|
||||
clusters.Thermostat.Attributes.OccupiedSetbackMax,
|
||||
),
|
||||
featuremap_contains=(clusters.Thermostat.Bitmaps.Feature.kSetback),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
|
||||
@@ -217,6 +217,9 @@
|
||||
"led_indicator_intensity_on": {
|
||||
"name": "LED on intensity"
|
||||
},
|
||||
"occupied_setback": {
|
||||
"name": "Occupied setback"
|
||||
},
|
||||
"off_transition_time": {
|
||||
"name": "Off transition time"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ from mill_local import OperationMode
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
@@ -111,13 +112,16 @@ class MillHeater(MillBaseEntity, ClimateEntity):
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
"""Set new target temperature and optionally HVAC mode."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
await self.coordinator.mill_data_connection.set_heater_temp(
|
||||
self._id, float(temperature)
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
|
||||
await self.async_handle_set_hvac_mode_service(hvac_mode)
|
||||
else:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
@@ -125,12 +129,11 @@ class MillHeater(MillBaseEntity, ClimateEntity):
|
||||
await self.coordinator.mill_data_connection.heater_control(
|
||||
self._id, power_status=True
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
await self.coordinator.mill_data_connection.heater_control(
|
||||
self._id, power_status=False
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _update_attr(self, device: mill.Heater) -> None:
|
||||
@@ -189,25 +192,26 @@ class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntit
|
||||
self._update_attr()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
"""Set new target temperature and optionally HVAC mode."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
await self.coordinator.mill_data_connection.set_target_temperature(
|
||||
float(temperature)
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
|
||||
await self.async_handle_set_hvac_mode_service(hvac_mode)
|
||||
else:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
await self.coordinator.mill_data_connection.set_operation_mode_control_individually()
|
||||
await self.coordinator.async_request_refresh()
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
await self.coordinator.mill_data_connection.set_operation_mode_off()
|
||||
await self.coordinator.async_request_refresh()
|
||||
elif hvac_mode == HVACMode.AUTO:
|
||||
await self.coordinator.mill_data_connection.set_operation_mode_weekly_program()
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
||||
@@ -47,7 +47,6 @@ rules:
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Use load_json_object_fixture in tests
|
||||
Patch the library instead of the HTTP requests
|
||||
Create a shared fixture for the mock config entry
|
||||
Use init_integration in tests
|
||||
|
||||
@@ -28,7 +28,7 @@ DEVICE_SUPPORT = {
|
||||
"3A": (),
|
||||
"3B": (),
|
||||
"42": (),
|
||||
"7E": ("EDS0066", "EDS0068"),
|
||||
"7E": ("EDS0065", "EDS0066", "EDS0068"),
|
||||
"A6": (),
|
||||
"EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"),
|
||||
}
|
||||
|
||||
@@ -297,6 +297,20 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
|
||||
# 7E sensors are special sensors by Embedded Data Systems
|
||||
|
||||
EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
|
||||
"EDS0065": (
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0065/temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0065/humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
),
|
||||
"EDS0066": (
|
||||
OneWireSensorEntityDescription(
|
||||
key="EDS0066/temperature",
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.16.1"]
|
||||
"requirements": ["opower==0.16.2"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from aiohttp import ClientError, ClientResponseError, web
|
||||
from pypoint import PointSession
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
@@ -21,14 +20,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import api
|
||||
from .const import CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, SIGNAL_WEBHOOK
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
type PointConfigEntry = ConfigEntry[PointDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
|
||||
"""Set up Minut Point from a config entry."""
|
||||
@@ -59,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> boo
|
||||
|
||||
point_session = PointSession(auth)
|
||||
|
||||
coordinator = PointDataUpdateCoordinator(hass, point_session)
|
||||
coordinator = PointDataUpdateCoordinator(hass, point_session, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PointConfigEntry
|
||||
from .const import DOMAIN, SIGNAL_WEBHOOK
|
||||
from .coordinator import PointConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PointConfigEntry
|
||||
from .const import SIGNAL_WEBHOOK
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
from .entity import MinutPointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from pypoint import PointSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
@@ -15,17 +16,24 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PointConfigEntry = ConfigEntry[PointDataUpdateCoordinator]
|
||||
|
||||
|
||||
class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
"""Class to manage fetching Point data from the API."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, point: PointSession) -> None:
|
||||
config_entry: PointConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, point: PointSession, config_entry: PointConfigEntry
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.point = point
|
||||
self.device_updates: dict[str, datetime] = {}
|
||||
|
||||
@@ -14,8 +14,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import PointConfigEntry
|
||||
from .coordinator import PointDataUpdateCoordinator
|
||||
from .coordinator import PointConfigEntry, PointDataUpdateCoordinator
|
||||
from .entity import MinutPointEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pooldose",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-pooldose==0.8.1"]
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["python-pooldose==0.8.2"]
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ rules:
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: This integration does not support dynamic device discovery, as each config entry represents a single PoolDose device with all available entities.
|
||||
|
||||
@@ -40,12 +40,13 @@ class PortainerSwitchEntityDescription(SwitchEntityDescription):
|
||||
async def perform_action(
|
||||
action: str, portainer: Portainer, endpoint_id: int, container_id: str
|
||||
) -> None:
|
||||
"""Stop a container."""
|
||||
"""Perform an action on a container."""
|
||||
try:
|
||||
if action == "start":
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
elif action == "stop":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
match action:
|
||||
case "start":
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
case "stop":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
except PortainerAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -142,6 +142,8 @@ async def async_setup_entry(
|
||||
class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEntity):
|
||||
"""Representation of a web scrape sensor."""
|
||||
|
||||
_sensor_name: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -162,14 +164,26 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
|
||||
self._value_template = value_template
|
||||
self._attr_native_value = None
|
||||
if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)):
|
||||
self._attr_name = None
|
||||
self._sensor_name = None
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer="Scrape",
|
||||
name=self.name,
|
||||
name=self._rendered[CONF_NAME],
|
||||
)
|
||||
else:
|
||||
self._sensor_name = self._rendered.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Return the name of the sensor.
|
||||
|
||||
Override needed because TriggerBaseEntity.name always returns the
|
||||
rendered name, ignoring _attr_name. When has_entity_name is True,
|
||||
we need name to return None to use the device name instead.
|
||||
"""
|
||||
return self._sensor_name
|
||||
|
||||
def _extract_value(self) -> Any:
|
||||
"""Parse the html extraction in the executor."""
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tplink_omada",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["tplink-omada-client==1.5.3"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==8.0.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==8.1.1", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vallox",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["vallox_websocket_api"],
|
||||
"requirements": ["vallox-websocket-api==5.3.0"]
|
||||
"requirements": ["vallox-websocket-api==6.0.0"]
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR,
|
||||
should_update_entity=lambda value: value is not None,
|
||||
@@ -251,9 +252,11 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
|
||||
# mcu temperature
|
||||
YoLinkSensorEntityDescription(
|
||||
key="devTemperature",
|
||||
translation_key="device_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
exists_fn=lambda device: device.device_type in MCU_DEV_TEMPERATURE_SENSOR,
|
||||
should_update_entity=lambda value: value is not None,
|
||||
value=lambda device, data: data.get("devTemperature"),
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"current_power": {
|
||||
"name": "Current power"
|
||||
},
|
||||
"device_temperature": {
|
||||
"name": "Device temperature"
|
||||
},
|
||||
"power_consumption": {
|
||||
"name": "Power consumption"
|
||||
},
|
||||
|
||||
@@ -38,12 +38,7 @@ from homeassistant.setup import SetupPhases, async_start_setup
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from . import (
|
||||
device_registry as dev_reg,
|
||||
entity_registry as ent_reg,
|
||||
service,
|
||||
translation,
|
||||
)
|
||||
from . import device_registry as dr, entity_registry as er, service, translation
|
||||
from .deprecation import deprecated_function
|
||||
from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider
|
||||
from .event import async_call_later
|
||||
@@ -624,7 +619,7 @@ class EntityPlatform:
|
||||
event loop and will finish faster if we run them concurrently.
|
||||
"""
|
||||
results: list[BaseException | None] | None = None
|
||||
entity_registry = ent_reg.async_get(self.hass)
|
||||
entity_registry = er.async_get(self.hass)
|
||||
try:
|
||||
async with self.hass.timeout.async_timeout(timeout, self.domain):
|
||||
results = await asyncio.gather(
|
||||
@@ -676,7 +671,7 @@ class EntityPlatform:
|
||||
to the event loop so we can await the coros directly without
|
||||
scheduling them as tasks.
|
||||
"""
|
||||
entity_registry = ent_reg.async_get(self.hass)
|
||||
entity_registry = er.async_get(self.hass)
|
||||
try:
|
||||
async with self.hass.timeout.async_timeout(timeout, self.domain):
|
||||
for entity in entities:
|
||||
@@ -852,16 +847,16 @@ class EntityPlatform:
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
device: dev_reg.DeviceEntry | None
|
||||
device: dr.DeviceEntry | None
|
||||
if self.config_entry:
|
||||
if device_info := entity.device_info:
|
||||
try:
|
||||
device = dev_reg.async_get(self.hass).async_get_or_create(
|
||||
device = dr.async_get(self.hass).async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
config_subentry_id=config_subentry_id,
|
||||
**device_info,
|
||||
)
|
||||
except dev_reg.DeviceInfoError as exc:
|
||||
except dr.DeviceInfoError as exc:
|
||||
self.logger.error(
|
||||
"%s: Not adding entity with invalid device info: %s",
|
||||
self.platform_name,
|
||||
@@ -869,6 +864,8 @@ class EntityPlatform:
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
entity.device_entry = device
|
||||
else:
|
||||
device = entity.device_entry
|
||||
else:
|
||||
@@ -929,8 +926,6 @@ class EntityPlatform:
|
||||
)
|
||||
|
||||
entity.registry_entry = entry
|
||||
if device:
|
||||
entity.device_entry = device
|
||||
entity.entity_id = entry.entity_id
|
||||
|
||||
else: # entity.unique_id is None
|
||||
@@ -1236,7 +1231,7 @@ class EntityPlatform:
|
||||
|
||||
@callback
|
||||
def async_calculate_suggested_object_id(
|
||||
entity: Entity, device: dev_reg.DeviceEntry | None
|
||||
entity: Entity, device: dr.DeviceEntry | None
|
||||
) -> str | None:
|
||||
"""Calculate the suggested object ID for an entity."""
|
||||
calculated_object_id: str | None = None
|
||||
|
||||
@@ -36,10 +36,10 @@ fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.8.0
|
||||
hass-nabucasa==1.7.0
|
||||
hass-nabucasa==1.9.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -9,8 +9,6 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
from .core import HomeAssistant, callback
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import singleton
|
||||
@@ -260,8 +258,13 @@ class RequirementsManager:
|
||||
"""
|
||||
if DEPRECATED_PACKAGES or self.hass.config.skip_pip_packages:
|
||||
all_requirements = {
|
||||
requirement_string: Requirement(requirement_string)
|
||||
requirement_string: requirement_details
|
||||
for requirement_string in requirements
|
||||
if (
|
||||
requirement_details := pkg_util.parse_requirement_safe(
|
||||
requirement_string
|
||||
)
|
||||
)
|
||||
}
|
||||
if DEPRECATED_PACKAGES:
|
||||
for requirement_string, requirement_details in all_requirements.items():
|
||||
@@ -272,9 +275,12 @@ class RequirementsManager:
|
||||
"" if is_built_in else "custom ",
|
||||
name,
|
||||
f"has requirement '{requirement_string}' which {reason}",
|
||||
f"This will stop working in Home Assistant {breaks_in_ha_version}, please"
|
||||
if breaks_in_ha_version
|
||||
else "Please",
|
||||
(
|
||||
"This will stop working in Home Assistant "
|
||||
f"{breaks_in_ha_version}, please"
|
||||
if breaks_in_ha_version
|
||||
else "Please"
|
||||
),
|
||||
async_suggest_report_issue(
|
||||
self.hass, integration_domain=name
|
||||
),
|
||||
|
||||
@@ -44,6 +44,39 @@ def get_installed_versions(specifiers: set[str]) -> set[str]:
|
||||
return {specifier for specifier in specifiers if is_installed(specifier)}
|
||||
|
||||
|
||||
def parse_requirement_safe(requirement_str: str) -> Requirement | None:
|
||||
"""Parse a requirement string into a Requirement object.
|
||||
|
||||
expected input is a pip compatible package specifier (requirement string)
|
||||
e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0" or "package@git+https://..."
|
||||
|
||||
For backward compatibility, it also accepts a URL with a fragment
|
||||
e.g. "git+https://github.com/pypa/pip#pip>=1"
|
||||
|
||||
Returns None on a badly-formed requirement string.
|
||||
"""
|
||||
try:
|
||||
return Requirement(requirement_str)
|
||||
except InvalidRequirement:
|
||||
if "#" not in requirement_str:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return None
|
||||
|
||||
# This is likely a URL with a fragment
|
||||
# example: git+https://github.com/pypa/pip#pip>=1
|
||||
|
||||
# fragment support was originally used to install zip files, and
|
||||
# we no longer do this in Home Assistant. However, custom
|
||||
# components started using it to install packages from git
|
||||
# urls which would make it would be a breaking change to
|
||||
# remove it.
|
||||
try:
|
||||
return Requirement(urlparse(requirement_str).fragment)
|
||||
except InvalidRequirement:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return None
|
||||
|
||||
|
||||
def is_installed(requirement_str: str) -> bool:
|
||||
"""Check if a package is installed and will be loaded when we import it.
|
||||
|
||||
@@ -56,26 +89,8 @@ def is_installed(requirement_str: str) -> bool:
|
||||
Returns True when the requirement is met.
|
||||
Returns False when the package is not installed or doesn't meet req.
|
||||
"""
|
||||
try:
|
||||
req = Requirement(requirement_str)
|
||||
except InvalidRequirement:
|
||||
if "#" not in requirement_str:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return False
|
||||
|
||||
# This is likely a URL with a fragment
|
||||
# example: git+https://github.com/pypa/pip#pip>=1
|
||||
|
||||
# fragment support was originally used to install zip files, and
|
||||
# we no longer do this in Home Assistant. However, custom
|
||||
# components started using it to install packages from git
|
||||
# urls which would make it would be a breaking change to
|
||||
# remove it.
|
||||
try:
|
||||
req = Requirement(urlparse(requirement_str).fragment)
|
||||
except InvalidRequirement:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return False
|
||||
if (req := parse_requirement_safe(requirement_str)) is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
if (installed_version := version(req.name)) is None:
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.7.0",
|
||||
"hass-nabucasa==1.9.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -24,7 +24,7 @@ cronsim==2.7
|
||||
cryptography==46.0.2
|
||||
fnv-hash-fast==1.6.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.7.0
|
||||
hass-nabucasa==1.9.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.1.6
|
||||
|
||||
16
requirements_all.txt
generated
16
requirements_all.txt
generated
@@ -1172,7 +1172,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.7.0
|
||||
hass-nabucasa==1.9.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -1216,7 +1216,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.6
|
||||
@@ -1684,7 +1684,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.16.1
|
||||
opower==0.16.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
@@ -2481,7 +2481,7 @@ python-awair==0.2.5
|
||||
python-blockchain-api==0.0.2
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==3.1.6
|
||||
python-bsblan==4.1.0
|
||||
|
||||
# homeassistant.components.citybikes
|
||||
python-citybikes==0.3.3
|
||||
@@ -2520,7 +2520,7 @@ python-google-weather-api==0.0.4
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==10.0.0
|
||||
python-homewizard-energy==10.0.1
|
||||
|
||||
# homeassistant.components.hp_ilo
|
||||
python-hpilo==4.4.3
|
||||
@@ -2575,7 +2575,7 @@ python-overseerr==0.8.0
|
||||
python-picnic-api2==1.3.1
|
||||
|
||||
# homeassistant.components.pooldose
|
||||
python-pooldose==0.8.1
|
||||
python-pooldose==0.8.2
|
||||
|
||||
# homeassistant.components.rabbitair
|
||||
python-rabbitair==0.0.8
|
||||
@@ -3081,7 +3081,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==8.0.0
|
||||
uiprotect==8.1.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -3116,7 +3116,7 @@ uvcclient==0.12.1
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
vallox-websocket-api==6.0.0
|
||||
|
||||
# homeassistant.components.vegehub
|
||||
vegehub==0.1.26
|
||||
|
||||
16
requirements_test_all.txt
generated
16
requirements_test_all.txt
generated
@@ -1042,7 +1042,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.7.0
|
||||
hass-nabucasa==1.9.0
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
@@ -1074,7 +1074,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.6
|
||||
@@ -1458,7 +1458,7 @@ openrgb-python==0.3.6
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.16.1
|
||||
opower==0.16.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
@@ -2098,7 +2098,7 @@ python-MotionMount==2.3.0
|
||||
python-awair==0.2.5
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==3.1.6
|
||||
python-bsblan==4.1.0
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.3.2
|
||||
@@ -2116,7 +2116,7 @@ python-google-weather-api==0.0.4
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==10.0.0
|
||||
python-homewizard-energy==10.0.1
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.2.9
|
||||
@@ -2165,7 +2165,7 @@ python-overseerr==0.8.0
|
||||
python-picnic-api2==1.3.1
|
||||
|
||||
# homeassistant.components.pooldose
|
||||
python-pooldose==0.8.1
|
||||
python-pooldose==0.8.2
|
||||
|
||||
# homeassistant.components.rabbitair
|
||||
python-rabbitair==0.0.8
|
||||
@@ -2575,7 +2575,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==8.0.0
|
||||
uiprotect==8.1.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -2604,7 +2604,7 @@ uvcclient==0.12.1
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
vallox-websocket-api==6.0.0
|
||||
|
||||
# homeassistant.components.vegehub
|
||||
vegehub==0.1.26
|
||||
|
||||
@@ -1390,7 +1390,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"freebox",
|
||||
"freedns",
|
||||
"freedompro",
|
||||
"fritz",
|
||||
"fritzbox",
|
||||
"fritzbox_callmonitor",
|
||||
"frontier_silicon",
|
||||
@@ -2000,7 +1999,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"touchline",
|
||||
"touchline_sl",
|
||||
"tplink_lte",
|
||||
"tplink_omada",
|
||||
"traccar",
|
||||
"traccar_server",
|
||||
"tractive",
|
||||
|
||||
@@ -167,9 +167,93 @@ class _StateDescription(TypedDict):
|
||||
class StateDescription(TypedDict):
|
||||
"""Test state and expected service call count."""
|
||||
|
||||
included: _StateDescription
|
||||
excluded: _StateDescription
|
||||
count: int
|
||||
included: _StateDescription # State for entities meant to be targeted
|
||||
excluded: _StateDescription # State for entities not meant to be targeted
|
||||
count: int # Expected service call count
|
||||
|
||||
|
||||
class ConditionStateDescription(TypedDict):
|
||||
"""Test state and expected service call count."""
|
||||
|
||||
included: _StateDescription # State for entities meant to be targeted
|
||||
excluded: _StateDescription # State for entities not meant to be targeted
|
||||
condition_true: bool # Whether the condition is expected to evaluate to true
|
||||
state_valid: bool # Whether the state is valid (not None, unavailable or unknown)
|
||||
|
||||
|
||||
def parametrize_condition_states(
|
||||
*,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize states and expected service call counts.
|
||||
|
||||
The target_states and other_states iterables are either iterables of
|
||||
states or iterables of (state, attributes) tuples.
|
||||
|
||||
Returns a list of tuples with (condition, condition options, list of states),
|
||||
where states is a list of ConditionStateDescription dicts.
|
||||
"""
|
||||
|
||||
additional_attributes = additional_attributes or {}
|
||||
condition_options = condition_options or {}
|
||||
|
||||
def state_with_attributes(
|
||||
state: str | None | tuple[str | None, dict],
|
||||
condition_true: bool,
|
||||
state_valid: bool,
|
||||
) -> ConditionStateDescription:
|
||||
"""Return ConditionStateDescription dict."""
|
||||
if isinstance(state, str) or state is None:
|
||||
return {
|
||||
"included": {
|
||||
"state": state,
|
||||
"attributes": additional_attributes,
|
||||
},
|
||||
"excluded": {
|
||||
"state": state,
|
||||
"attributes": {},
|
||||
},
|
||||
"condition_true": condition_true,
|
||||
"state_valid": state_valid,
|
||||
}
|
||||
return {
|
||||
"included": {
|
||||
"state": state[0],
|
||||
"attributes": state[1] | additional_attributes,
|
||||
},
|
||||
"excluded": {
|
||||
"state": state[0],
|
||||
"attributes": state[1],
|
||||
},
|
||||
"condition_true": condition_true,
|
||||
"state_valid": state_valid,
|
||||
}
|
||||
|
||||
return [
|
||||
(
|
||||
condition,
|
||||
condition_options,
|
||||
list(
|
||||
itertools.chain(
|
||||
(state_with_attributes(None, False, False),),
|
||||
(state_with_attributes(STATE_UNAVAILABLE, False, False),),
|
||||
(state_with_attributes(STATE_UNKNOWN, False, False),),
|
||||
(
|
||||
state_with_attributes(other_state, False, True)
|
||||
for other_state in other_states
|
||||
),
|
||||
(
|
||||
state_with_attributes(target_state, True, True)
|
||||
for target_state in target_states
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_trigger_states(
|
||||
@@ -202,8 +286,8 @@ def parametrize_trigger_states(
|
||||
|
||||
def state_with_attributes(
|
||||
state: str | None | tuple[str | None, dict], count: int
|
||||
) -> dict:
|
||||
"""Return (state, attributes) dict."""
|
||||
) -> StateDescription:
|
||||
"""Return StateDescription dict."""
|
||||
if isinstance(state, str) or state is None:
|
||||
return {
|
||||
"included": {
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairzone.common import OperationMode
|
||||
from aioairzone.common import OperationMode, QAdapt
|
||||
from aioairzone.const import (
|
||||
API_COLD_ANGLE,
|
||||
API_DATA,
|
||||
API_HEAT_ANGLE,
|
||||
API_MODE,
|
||||
API_Q_ADAPT,
|
||||
API_SLEEP,
|
||||
API_SYSTEM_ID,
|
||||
API_ZONE_ID,
|
||||
@@ -17,7 +18,7 @@ import pytest
|
||||
from homeassistant.components.select import ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
@@ -27,6 +28,11 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None:
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
# Systems
|
||||
state = hass.states.get("select.system_1_q_adapt")
|
||||
assert state.state == "standard"
|
||||
|
||||
# Zones
|
||||
state = hass.states.get("select.despacho_cold_angle")
|
||||
assert state.state == "90deg"
|
||||
|
||||
@@ -95,6 +101,71 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None:
|
||||
assert state.state == "off"
|
||||
|
||||
|
||||
async def test_airzone_select_sys_qadapt(hass: HomeAssistant) -> None:
|
||||
"""Test select system Q-Adapt."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
put_q_adapt = {
|
||||
API_DATA: {
|
||||
API_SYSTEM_ID: 1,
|
||||
API_Q_ADAPT: QAdapt.SILENCE,
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.system_1_q_adapt",
|
||||
ATTR_OPTION: "Invalid",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
|
||||
return_value=put_q_adapt,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.system_1_q_adapt",
|
||||
ATTR_OPTION: "silence",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("select.system_1_q_adapt")
|
||||
assert state.state == "silence"
|
||||
|
||||
put_q_adapt = {
|
||||
API_DATA: {
|
||||
API_SYSTEM_ID: 2,
|
||||
API_Q_ADAPT: QAdapt.SILENCE,
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
|
||||
return_value=put_q_adapt,
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.system_1_q_adapt",
|
||||
ATTR_OPTION: "silence",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_airzone_select_sleep(hass: HomeAssistant) -> None:
|
||||
"""Test select sleep."""
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ CONF_DATA: Final = {CONF_HOST: "test", CONF_PORT: 1234}
|
||||
|
||||
MOCK_STATUS: Final = {
|
||||
"APC": "001,038,0985",
|
||||
"DATE": "1970-01-01 00:00:00 0000",
|
||||
"DATE": "1970-01-01 00:00:00 +0000",
|
||||
"VERSION": "3.14.14 (31 May 2016) unknown",
|
||||
"CABLE": "USB Cable",
|
||||
"DRIVER": "USB UPS Driver",
|
||||
@@ -19,6 +19,7 @@ MOCK_STATUS: Final = {
|
||||
"APCMODEL": "Back-UPS ES 600",
|
||||
"MODEL": "Back-UPS ES 600",
|
||||
"STATUS": "ONLINE",
|
||||
"STARTTIME": "2006-01-01 00:00:00 +0500",
|
||||
"LINEV": "124.0 Volts",
|
||||
"LOADPCT": "14.0 Percent",
|
||||
"BCHARGE": "100.0 Percent",
|
||||
@@ -36,11 +37,11 @@ MOCK_STATUS: Final = {
|
||||
"OUTCURNT": "0.88 Amps",
|
||||
"LASTXFER": "Automatic or explicit self test",
|
||||
"NUMXFERS": "1",
|
||||
"XONBATT": "1970-01-01 00:00:00 0000",
|
||||
"XONBATT": "1970-01-01 00:00:00 +0000",
|
||||
"TONBATT": "0 Seconds",
|
||||
"CUMONBATT": "8 Seconds",
|
||||
"XOFFBATT": "1970-01-01 00:00:00 0000",
|
||||
"LASTSTEST": "1970-01-01 00:00:00 0000",
|
||||
"XOFFBATT": "1970-01-01 00:00:00 +0000",
|
||||
"LASTSTEST": "1970-01-01 00:00:00 +0000",
|
||||
"SELFTEST": "NO",
|
||||
"STESTI": "7 days",
|
||||
"STATFLAG": "0x05000008",
|
||||
@@ -50,7 +51,8 @@ MOCK_STATUS: Final = {
|
||||
"NOMBATTV": "12.0 Volts",
|
||||
"NOMPOWER": "330 Watts",
|
||||
"FIRMWARE": "928.a8 .D USB FW:a8",
|
||||
"END APC": "1970-01-01 00:00:00 0000",
|
||||
"MASTERUPD": "1970-01-01 00:00:00 +0000",
|
||||
"END APC": "1970-01-01 00:00:00 +0000",
|
||||
}
|
||||
|
||||
# Minimal status adapted from http://www.apcupsd.org/manual/manual.html#apcaccess-test.
|
||||
@@ -58,13 +60,13 @@ MOCK_STATUS: Final = {
|
||||
# of the integration to handle such cases.
|
||||
MOCK_MINIMAL_STATUS: Final = {
|
||||
"APC": "001,012,0319",
|
||||
"DATE": "1970-01-01 00:00:00 0000",
|
||||
"DATE": "1970-01-01 00:00:00 +0000",
|
||||
"RELEASE": "3.8.5",
|
||||
"CABLE": "APC Cable 940-0128A",
|
||||
"UPSMODE": "Stand Alone",
|
||||
"STARTTIME": "1970-01-01 00:00:00 0000",
|
||||
"STARTTIME": "1970-01-01 00:00:00 +0000",
|
||||
"LINEFAIL": "OK",
|
||||
"BATTSTAT": "OK",
|
||||
"STATFLAG": "0x008",
|
||||
"END APC": "1970-01-01 00:00:00 0000",
|
||||
"END APC": "1970-01-01 00:00:00 +0000",
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@
|
||||
'BCHARGE': '100.0 Percent',
|
||||
'CABLE': 'USB Cable',
|
||||
'CUMONBATT': '8 Seconds',
|
||||
'DATE': '1970-01-01 00:00:00 0000',
|
||||
'DATE': '1970-01-01 00:00:00 +0000',
|
||||
'DRIVER': 'USB UPS Driver',
|
||||
'END APC': '1970-01-01 00:00:00 0000',
|
||||
'END APC': '1970-01-01 00:00:00 +0000',
|
||||
'FIRMWARE': '928.a8 .D USB FW:a8',
|
||||
'HITRANS': '139.0 Volts',
|
||||
'ITEMP': '34.6 C Internal',
|
||||
'LASTSTEST': '1970-01-01 00:00:00 0000',
|
||||
'LASTSTEST': '1970-01-01 00:00:00 +0000',
|
||||
'LASTXFER': 'Automatic or explicit self test',
|
||||
'LINEV': '124.0 Volts',
|
||||
'LOADPCT': '14.0 Percent',
|
||||
'LOTRANS': '92.0 Volts',
|
||||
'MASTERUPD': '1970-01-01 00:00:00 +0000',
|
||||
'MAXTIME': '0 Seconds',
|
||||
'MBATTCHG': '5 Percent',
|
||||
'MINTIMEL': '3 Minutes',
|
||||
@@ -33,6 +34,7 @@
|
||||
'SELFTEST': 'NO',
|
||||
'SENSE': 'Medium',
|
||||
'SERIALNO': '**REDACTED**',
|
||||
'STARTTIME': '2006-01-01 00:00:00 +0500',
|
||||
'STATFLAG': '0x05000008',
|
||||
'STATUS': 'ONLINE',
|
||||
'STESTI': '7 days',
|
||||
@@ -41,7 +43,7 @@
|
||||
'UPSMODE': 'Stand Alone',
|
||||
'UPSNAME': 'MyUPS',
|
||||
'VERSION': '3.14.14 (31 May 2016) unknown',
|
||||
'XOFFBATT': '1970-01-01 00:00:00 0000',
|
||||
'XONBATT': '1970-01-01 00:00:00 0000',
|
||||
'XOFFBATT': '1970-01-01 00:00:00 +0000',
|
||||
'XONBATT': '1970-01-01 00:00:00 +0000',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -737,7 +737,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01 00:00:00 0000',
|
||||
'state': '1970-01-01 00:00:00 +0000',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_driver-entry]
|
||||
@@ -971,7 +971,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last self-test',
|
||||
'platform': 'apcupsd',
|
||||
@@ -986,6 +986,7 @@
|
||||
# name: test_sensor[sensor.myups_last_self_test-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'MyUPS Last self-test',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -993,7 +994,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01 00:00:00 0000',
|
||||
'state': '1970-01-01T00:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_last_transfer-entry]
|
||||
@@ -1096,6 +1097,55 @@
|
||||
'state': '14.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_master_update-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.myups_master_update',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Master update',
|
||||
'platform': 'apcupsd',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'master_update',
|
||||
'unique_id': 'XXXXXXXXXXXX_masterupd',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_master_update-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'MyUPS Master update',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.myups_master_update',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01T00:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1745,6 +1795,55 @@
|
||||
'state': '3',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_startup_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_startup_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Startup time',
|
||||
'platform': 'apcupsd',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'startup_time',
|
||||
'unique_id': 'XXXXXXXXXXXX_starttime',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_startup_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'MyUPS Startup time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.myups_startup_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2005-12-31T19:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1886,7 +1985,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01 00:00:00 0000',
|
||||
'state': '1970-01-01 00:00:00 +0000',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_status_flag-entry]
|
||||
@@ -2179,7 +2278,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Transfer from battery',
|
||||
'platform': 'apcupsd',
|
||||
@@ -2194,6 +2293,7 @@
|
||||
# name: test_sensor[sensor.myups_transfer_from_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'MyUPS Transfer from battery',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -2201,7 +2301,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01 00:00:00 0000',
|
||||
'state': '1970-01-01T00:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_transfer_high-entry]
|
||||
@@ -2333,7 +2433,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Transfer to battery',
|
||||
'platform': 'apcupsd',
|
||||
@@ -2348,6 +2448,7 @@
|
||||
# name: test_sensor[sensor.myups_transfer_to_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'MyUPS Transfer to battery',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -2355,6 +2456,6 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1970-01-01 00:00:00 0000',
|
||||
'state': '1970-01-01T00:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import dateutil.parser
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -151,13 +152,17 @@ async def test_sensor_unknown(
|
||||
|
||||
# Simulate an event (a self test) such that "LASTSTEST" field is being reported, the state of
|
||||
# the sensor should be properly updated with the corresponding value.
|
||||
last_self_test_value = "1970-01-01 00:00:00 +0000"
|
||||
mock_request_status.return_value = MOCK_MINIMAL_STATUS | {
|
||||
"LASTSTEST": "1970-01-01 00:00:00 0000"
|
||||
"LASTSTEST": last_self_test_value
|
||||
}
|
||||
future = utcnow() + timedelta(minutes=2)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(last_self_test_id).state == "1970-01-01 00:00:00 0000"
|
||||
assert (
|
||||
hass.states.get(last_self_test_id).state
|
||||
== dateutil.parser.parse(last_self_test_value).isoformat()
|
||||
)
|
||||
|
||||
# Simulate another event (e.g., daemon restart) such that "LASTSTEST" is no longer reported.
|
||||
mock_request_status.return_value = MOCK_MINIMAL_STATUS
|
||||
|
||||
@@ -159,6 +159,30 @@ async def test_climate_hvac_mode_none_value(
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
async def test_climate_hvac_mode_object_none(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test climate entity when hvac_mode object itself is None."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# Set hvac_mode to None (the object itself, not just the value)
|
||||
mock_bsblan.state.return_value.hvac_mode = None
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# State should be unknown when hvac_mode object is None
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == "unknown"
|
||||
# preset_mode should be "none" when hvac_mode object is None
|
||||
assert state.attributes["preset_mode"] == PRESET_NONE
|
||||
|
||||
|
||||
async def test_climate_hvac_mode_string_fallback(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
|
||||
@@ -245,6 +245,7 @@ async def cloud_prefs(hass: HomeAssistant) -> CloudPreferences:
|
||||
async def mock_cloud_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the cloud."""
|
||||
await mock_cloud(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -130,7 +130,6 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None:
|
||||
"relayer_server": "relayer",
|
||||
"acme_server": "cert-server",
|
||||
"api_server": "api-test.example.com",
|
||||
"accounts_server": "api-test.hass.io",
|
||||
"google_actions": {"filter": {"include_domains": "light"}},
|
||||
"alexa": {
|
||||
"filter": {"include_entities": ["light.kitchen", "switch.ac"]}
|
||||
|
||||
@@ -45,7 +45,6 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
|
||||
"region": "test-region",
|
||||
"api_server": "test-api-server",
|
||||
"relayer_server": "test-relayer-server",
|
||||
"accounts_server": "test-acounts-server",
|
||||
"acme_server": "test-acme-server",
|
||||
"remotestate_server": "test-remotestate-server",
|
||||
"discovery_service_actions": {
|
||||
@@ -63,7 +62,6 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
|
||||
assert cl.region == "test-region"
|
||||
assert cl.relayer_server == "test-relayer-server"
|
||||
assert cl.iot.ws_server_url == "wss://test-relayer-server/websocket"
|
||||
assert cl.accounts_server == "test-acounts-server"
|
||||
assert cl.acme_server == "test-acme-server"
|
||||
assert cl.api_server == "test-api-server"
|
||||
assert cl.remotestate_server == "test-remotestate-server"
|
||||
|
||||
@@ -48,7 +48,7 @@ async def test_create_repair_issues_at_startup_if_logged_in(
|
||||
) -> None:
|
||||
"""Test that we create repair issue at startup if we are logged in."""
|
||||
aioclient_mock.get(
|
||||
"https://accounts.nabucasa.com/payments/subscription_info",
|
||||
"https://api.nabucasa.com/account/payments/subscription_info",
|
||||
json={"provider": "legacy"},
|
||||
)
|
||||
|
||||
@@ -88,11 +88,11 @@ async def test_legacy_subscription_repair_flow(
|
||||
) -> None:
|
||||
"""Test desired flow of the fix flow for legacy subscription."""
|
||||
aioclient_mock.get(
|
||||
"https://accounts.nabucasa.com/payments/subscription_info",
|
||||
"https://api.nabucasa.com/account/payments/subscription_info",
|
||||
json={"provider": None},
|
||||
)
|
||||
aioclient_mock.post(
|
||||
"https://accounts.nabucasa.com/payments/migrate_paypal_agreement",
|
||||
"https://api.nabucasa.com/account/payments/migrate_paypal_agreement",
|
||||
json={"url": "https://paypal.com"},
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
async def mocked_cloud_object(hass: HomeAssistant) -> Cloud:
|
||||
"""Mock cloud object."""
|
||||
return Mock(
|
||||
accounts_server="accounts.nabucasa.com",
|
||||
auth=Mock(async_check_token=AsyncMock()),
|
||||
websession=async_get_clientsession(hass),
|
||||
payments=Mock(
|
||||
|
||||
@@ -345,10 +345,10 @@ async def test_get_tts_audio_logged_out(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_process_tts_side_effect"),
|
||||
"mock_process_tts_side_effect",
|
||||
[
|
||||
(None,),
|
||||
(VoiceError("Boom!"),),
|
||||
None,
|
||||
VoiceError("Boom!"),
|
||||
],
|
||||
)
|
||||
async def test_tts_entity(
|
||||
|
||||
@@ -823,6 +823,9 @@ async def _check_config_flow_result_translations(
|
||||
integration = flow.handler
|
||||
issue_id = flow.issue_id
|
||||
issue = ir.async_get(flow.hass).async_get_issue(integration, issue_id)
|
||||
if issue is None:
|
||||
# Issue was deleted mid-flow (e.g., config entry removed), skip check
|
||||
return
|
||||
key_prefix = f"{issue.translation_key}.fix_flow."
|
||||
description_placeholders = {
|
||||
# Both are used in issue translations, and description_placeholders
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
'attribution': 'Data provided by Fitbit.com',
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'First L. Sleep time in bed',
|
||||
'icon': 'mdi:hotel',
|
||||
'icon': 'mdi:bed',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.printer_wake_on_lan',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -218,13 +218,13 @@
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:lan-pending',
|
||||
'original_name': 'printer Wake on LAN',
|
||||
'original_icon': None,
|
||||
'original_name': 'Wake on LAN',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'translation_key': 'wake_on_lan',
|
||||
'unique_id': 'AA:BB:CC:00:11:22_wake_on_lan',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -233,7 +233,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'printer Wake on LAN',
|
||||
'icon': 'mdi:lan-pending',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.printer_wake_on_lan',
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -121,13 +121,13 @@
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:router-wireless-settings',
|
||||
'original_name': 'printer Internet Access',
|
||||
'original_icon': None,
|
||||
'original_name': 'Internet access',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'translation_key': 'internet_access',
|
||||
'unique_id': 'AA:BB:CC:00:11:22_internet_access',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -135,8 +135,7 @@
|
||||
# name: test_switch_setup[fc_data0][switch.printer_internet_access-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'printer Internet Access',
|
||||
'icon': 'mdi:router-wireless-settings',
|
||||
'friendly_name': 'printer Internet access',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
@@ -258,7 +257,7 @@
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -268,13 +267,13 @@
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:router-wireless-settings',
|
||||
'original_name': 'printer Internet Access',
|
||||
'original_icon': None,
|
||||
'original_name': 'Internet access',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'translation_key': 'internet_access',
|
||||
'unique_id': 'AA:BB:CC:00:11:22_internet_access',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -282,8 +281,7 @@
|
||||
# name: test_switch_setup[fc_data1][switch.printer_internet_access-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'printer Internet Access',
|
||||
'icon': 'mdi:router-wireless-settings',
|
||||
'friendly_name': 'printer Internet access',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
@@ -405,7 +403,7 @@
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -415,13 +413,13 @@
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:router-wireless-settings',
|
||||
'original_name': 'printer Internet Access',
|
||||
'original_icon': None,
|
||||
'original_name': 'Internet access',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'translation_key': 'internet_access',
|
||||
'unique_id': 'AA:BB:CC:00:11:22_internet_access',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -429,8 +427,7 @@
|
||||
# name: test_switch_setup[fc_data2][switch.printer_internet_access-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'printer Internet Access',
|
||||
'icon': 'mdi:router-wireless-settings',
|
||||
'friendly_name': 'printer Internet access',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
@@ -558,7 +555,7 @@
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -568,13 +565,13 @@
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:router-wireless-settings',
|
||||
'original_name': 'printer Internet Access',
|
||||
'original_icon': None,
|
||||
'original_name': 'Internet access',
|
||||
'platform': 'fritz',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'translation_key': 'internet_access',
|
||||
'unique_id': 'AA:BB:CC:00:11:22_internet_access',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -582,8 +579,7 @@
|
||||
# name: test_switch_setup[fc_data3][switch.printer_internet_access-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'printer Internet Access',
|
||||
'icon': 'mdi:router-wireless-settings',
|
||||
'friendly_name': 'printer Internet access',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.printer_internet_access',
|
||||
|
||||
@@ -81,6 +81,42 @@
|
||||
"value": 1.2,
|
||||
"units": "PARTS_PER_BILLION"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "nh3",
|
||||
"displayName": "NH3",
|
||||
"fullName": "Ammonia",
|
||||
"concentration": {
|
||||
"value": 81.41,
|
||||
"units": "PARTS_PER_BILLION"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "no",
|
||||
"displayName": "NO",
|
||||
"fullName": "Nitrogen monoxide",
|
||||
"concentration": {
|
||||
"value": 0.62,
|
||||
"units": "PARTS_PER_BILLION"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "nmhc",
|
||||
"displayName": "NMHC",
|
||||
"fullName": "Non-methane hydrocarbons",
|
||||
"concentration": {
|
||||
"value": 52.66,
|
||||
"units": "PARTS_PER_BILLION"
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "c6h6",
|
||||
"displayName": "C6H6",
|
||||
"fullName": "Benzene",
|
||||
"concentration": {
|
||||
"value": 0.24,
|
||||
"units": "MICROGRAMS_PER_CUBIC_METER"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,110 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_snapshot[sensor.home_ammonia-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.home_ammonia',
|
||||
'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': 'Ammonia',
|
||||
'platform': 'google_air_quality',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ammonia',
|
||||
'unique_id': 'nh3_10.1_20.1',
|
||||
'unit_of_measurement': 'ppb',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_ammonia-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Google Air Quality',
|
||||
'friendly_name': 'Home Ammonia',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppb',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_ammonia',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '81.41',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_benzene-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.home_benzene',
|
||||
'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': 'Benzene',
|
||||
'platform': 'google_air_quality',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'benzene',
|
||||
'unique_id': 'c6h6_10.1_20.1',
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_benzene-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Google Air Quality',
|
||||
'friendly_name': 'Home Benzene',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'μg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_benzene',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.24',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_carbon_monoxide-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -234,6 +340,112 @@
|
||||
'state': '14.18',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_nitrogen_monoxide-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.home_nitrogen_monoxide',
|
||||
'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': 'Nitrogen monoxide',
|
||||
'platform': 'google_air_quality',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'nitrogen_monoxide',
|
||||
'unique_id': 'no_10.1_20.1',
|
||||
'unit_of_measurement': 'ppb',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_nitrogen_monoxide-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Google Air Quality',
|
||||
'friendly_name': 'Home Nitrogen monoxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppb',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_nitrogen_monoxide',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.62',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_non_methane_hydrocarbons-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.home_non_methane_hydrocarbons',
|
||||
'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': 'Non-methane hydrocarbons',
|
||||
'platform': 'google_air_quality',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'non_methane_hydrocarbons',
|
||||
'unique_id': 'nmhc_10.1_20.1',
|
||||
'unit_of_measurement': 'ppb',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_non_methane_hydrocarbons-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by Google Air Quality',
|
||||
'friendly_name': 'Home Non-methane hydrocarbons',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppb',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.home_non_methane_hydrocarbons',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '52.66',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.home_ozone-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
30
tests/components/hdfury/snapshots/test_diagnostics.ambr
Normal file
30
tests/components/hdfury/snapshots/test_diagnostics.ambr
Normal file
@@ -0,0 +1,30 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'board': dict({
|
||||
'hostname': 'VRROOM-02',
|
||||
'ipaddress': '192.168.1.123',
|
||||
'pcbv': '3',
|
||||
'serial': '000123456789',
|
||||
'version': 'FW: 0.61',
|
||||
}),
|
||||
'config': dict({
|
||||
'autosw': '1',
|
||||
'htpcmode0': '0',
|
||||
'htpcmode1': '0',
|
||||
'htpcmode2': '0',
|
||||
'htpcmode3': '0',
|
||||
'iractive': '1',
|
||||
'macaddr': 'c7:1c:df:9d:f6:40',
|
||||
'mutetx0': '1',
|
||||
'mutetx1': '1',
|
||||
'oled': '1',
|
||||
'relay': '0',
|
||||
}),
|
||||
'info': dict({
|
||||
'opmode': '0',
|
||||
'portseltx0': '0',
|
||||
'portseltx1': '4',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
31
tests/components/hdfury/test_diagnostics.py
Normal file
31
tests/components/hdfury/test_diagnostics.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Tests for the HDFury diagnostics."""
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.hdfury import PLATFORMS
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test HDFury diagnostics."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry, PLATFORMS)
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert diagnostics == snapshot
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test light conditions."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -13,24 +14,18 @@ from homeassistant.const import (
|
||||
CONF_TARGET,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.components import (
|
||||
ConditionStateDescription,
|
||||
parametrize_condition_states,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
INVALID_STATES = [
|
||||
{"state": STATE_UNAVAILABLE, "attributes": {}},
|
||||
{"state": STATE_UNKNOWN, "attributes": {}},
|
||||
{"state": None, "attributes": {}},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
@@ -76,15 +71,15 @@ async def setup_automation_with_light_condition(
|
||||
)
|
||||
|
||||
|
||||
async def has_call_after_trigger(
|
||||
async def has_single_call_after_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> bool:
|
||||
"""Check if there are service calls after the trigger event."""
|
||||
"""Check if there is a single service call after the trigger event."""
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
has_calls = len(service_calls) == 1
|
||||
num_calls = len(service_calls)
|
||||
service_calls.clear()
|
||||
return has_calls
|
||||
return num_calls == 1
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
@@ -125,17 +120,17 @@ async def test_light_conditions_gated_by_labs_flag(
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "target_state", "other_state"),
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
(
|
||||
"light.is_on",
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
(
|
||||
"light.is_off",
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -148,15 +143,15 @@ async def test_light_state_condition_behavior_any(
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
target_state: str,
|
||||
other_state: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light state condition with the 'any' behavior."""
|
||||
other_entity_ids = set(target_lights) - {entity_id}
|
||||
|
||||
# Set all lights, including the tested light, to the initial state
|
||||
for eid in target_lights:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await setup_automation_with_light_condition(
|
||||
@@ -167,38 +162,29 @@ async def test_light_state_condition_behavior_any(
|
||||
)
|
||||
|
||||
# Set state for switches to ensure that they don't impact the condition
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
await hass.async_block_till_done()
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
for state in states:
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, state["included"])
|
||||
await hass.async_block_till_done()
|
||||
assert not await has_single_call_after_trigger(hass, service_calls)
|
||||
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
await hass.async_block_till_done()
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
# Set one light to the condition state -> condition pass
|
||||
set_or_remove_state(hass, entity_id, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
# Set all remaining lights to the condition state -> condition pass
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set one light to the invalid state -> condition pass if there are
|
||||
# other lights in the condition state
|
||||
set_or_remove_state(hass, entity_id, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls) == bool(
|
||||
entities_in_target - 1
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
await has_single_call_after_trigger(hass, service_calls)
|
||||
== state["condition_true"]
|
||||
)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set all lights to invalid state -> condition fail
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, invalid_state)
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
# Check if changing other lights also passes the condition
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
await has_single_call_after_trigger(hass, service_calls)
|
||||
== state["condition_true"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@@ -207,17 +193,17 @@ async def test_light_state_condition_behavior_any(
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "target_state", "other_state"),
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
(
|
||||
"light.is_on",
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
(
|
||||
"light.is_off",
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -229,8 +215,8 @@ async def test_light_state_condition_behavior_all(
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
target_state: str,
|
||||
other_state: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light state condition with the 'all' behavior."""
|
||||
# Set state for two switches to ensure that they don't impact the condition
|
||||
@@ -241,7 +227,7 @@ async def test_light_state_condition_behavior_all(
|
||||
|
||||
# Set all lights, including the tested light, to the initial state
|
||||
for eid in target_lights:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await setup_automation_with_light_condition(
|
||||
@@ -251,27 +237,22 @@ async def test_light_state_condition_behavior_all(
|
||||
behavior="all",
|
||||
)
|
||||
|
||||
# No lights on the condition state
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
|
||||
# Set one light to the condition state -> condition fail
|
||||
set_or_remove_state(hass, entity_id, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls) == (
|
||||
entities_in_target == 1
|
||||
)
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert await has_single_call_after_trigger(hass, service_calls) == (
|
||||
(not state["state_valid"])
|
||||
or (state["condition_true"] and entities_in_target == 1)
|
||||
)
|
||||
|
||||
# Set all remaining lights to the condition state -> condition pass
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set one light to the invalid state -> condition still pass
|
||||
set_or_remove_state(hass, entity_id, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set all lights to unavailable -> condition passes
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert await has_single_call_after_trigger(hass, service_calls) == (
|
||||
(not state["state_valid"]) or state["condition_true"]
|
||||
)
|
||||
|
||||
@@ -96,7 +96,8 @@ async def integration_fixture(
|
||||
"eve_energy_20ecn4101",
|
||||
"eve_energy_plug",
|
||||
"eve_energy_plug_patched",
|
||||
"eve_thermo",
|
||||
"eve_thermo_v4",
|
||||
"eve_thermo_v5",
|
||||
"eve_shutter",
|
||||
"eve_weather_sensor",
|
||||
"extended_color_light",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"0/40/0": 17,
|
||||
"0/40/1": "Eve Systems",
|
||||
"0/40/2": 4874,
|
||||
"0/40/3": "Eve Thermo",
|
||||
"0/40/3": "Eve Thermo 20EBP1701",
|
||||
"0/40/4": 79,
|
||||
"0/40/5": "",
|
||||
"0/40/6": "**REDACTED**",
|
||||
593
tests/components/matter/fixtures/nodes/eve_thermo_v5.json
Normal file
593
tests/components/matter/fixtures/nodes/eve_thermo_v5.json
Normal file
@@ -0,0 +1,593 @@
|
||||
{
|
||||
"node_id": 12,
|
||||
"date_commissioned": "2026-01-12T17:05:18.823583",
|
||||
"last_interview": "2026-01-12T17:12:42.428644",
|
||||
"interview_version": 6,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
"attributes": {
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 18,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 17,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 22,
|
||||
"1": 3
|
||||
}
|
||||
],
|
||||
"0/29/1": [
|
||||
29, 31, 40, 42, 47, 48, 49, 50, 51, 52, 53, 56, 60, 62, 63, 70, 323615744
|
||||
],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1],
|
||||
"0/29/65532": 0,
|
||||
"0/29/65533": 2,
|
||||
"0/29/65528": [],
|
||||
"0/29/65529": [],
|
||||
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/31/0": [
|
||||
{
|
||||
"254": 1
|
||||
},
|
||||
{
|
||||
"254": 1
|
||||
},
|
||||
{
|
||||
"254": 2
|
||||
},
|
||||
{
|
||||
"254": 3
|
||||
},
|
||||
{
|
||||
"1": 5,
|
||||
"2": 2,
|
||||
"3": [112233],
|
||||
"4": null,
|
||||
"254": 4
|
||||
}
|
||||
],
|
||||
"0/31/1": [],
|
||||
"0/31/2": 10,
|
||||
"0/31/3": 3,
|
||||
"0/31/4": 5,
|
||||
"0/31/65532": 1,
|
||||
"0/31/65533": 2,
|
||||
"0/31/65528": [],
|
||||
"0/31/65529": [],
|
||||
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/40/0": 18,
|
||||
"0/40/1": "Eve Systems",
|
||||
"0/40/2": 4874,
|
||||
"0/40/3": "Eve Thermo 20ECD1701",
|
||||
"0/40/4": 125,
|
||||
"0/40/5": "",
|
||||
"0/40/6": "**REDACTED**",
|
||||
"0/40/7": 1,
|
||||
"0/40/8": "1.1",
|
||||
"0/40/9": 10287,
|
||||
"0/40/10": "3.6.5",
|
||||
"0/40/15": "FX46O1M01234",
|
||||
"0/40/18": "DF3D0B4137A71234",
|
||||
"0/40/19": {
|
||||
"0": 3,
|
||||
"1": 3
|
||||
},
|
||||
"0/40/21": 17039616,
|
||||
"0/40/22": 1,
|
||||
"0/40/65532": 0,
|
||||
"0/40/65533": 4,
|
||||
"0/40/65528": [],
|
||||
"0/40/65529": [],
|
||||
"0/40/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 21, 22, 65528, 65529, 65531,
|
||||
65532, 65533
|
||||
],
|
||||
"0/42/0": [
|
||||
{
|
||||
"1": 556220604,
|
||||
"2": 0,
|
||||
"254": 1
|
||||
}
|
||||
],
|
||||
"0/42/1": true,
|
||||
"0/42/2": 1,
|
||||
"0/42/3": null,
|
||||
"0/42/65532": 0,
|
||||
"0/42/65533": 1,
|
||||
"0/42/65528": [],
|
||||
"0/42/65529": [0],
|
||||
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/47/0": 1,
|
||||
"0/47/1": 0,
|
||||
"0/47/2": "Battery",
|
||||
"0/47/11": null,
|
||||
"0/47/12": 200,
|
||||
"0/47/14": 0,
|
||||
"0/47/15": false,
|
||||
"0/47/16": 2,
|
||||
"0/47/18": [],
|
||||
"0/47/19": "",
|
||||
"0/47/20": 2,
|
||||
"0/47/25": 2,
|
||||
"0/47/31": [],
|
||||
"0/47/65532": 10,
|
||||
"0/47/65533": 3,
|
||||
"0/47/65528": [],
|
||||
"0/47/65529": [],
|
||||
"0/47/65531": [
|
||||
0, 1, 2, 11, 12, 14, 15, 16, 18, 19, 20, 25, 31, 65528, 65529, 65531,
|
||||
65532, 65533
|
||||
],
|
||||
"0/48/0": 0,
|
||||
"0/48/1": {
|
||||
"0": 60,
|
||||
"1": 900
|
||||
},
|
||||
"0/48/2": 0,
|
||||
"0/48/3": 0,
|
||||
"0/48/4": true,
|
||||
"0/48/65532": 0,
|
||||
"0/48/65533": 2,
|
||||
"0/48/65528": [1, 3, 5],
|
||||
"0/48/65529": [0, 2, 4],
|
||||
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/49/0": 1,
|
||||
"0/49/1": [
|
||||
{
|
||||
"0": "p0jbsOzJRNw=",
|
||||
"1": true
|
||||
}
|
||||
],
|
||||
"0/49/2": 10,
|
||||
"0/49/3": 20,
|
||||
"0/49/4": true,
|
||||
"0/49/5": 0,
|
||||
"0/49/6": "p0jbsOzJRNw=",
|
||||
"0/49/7": null,
|
||||
"0/49/9": 4,
|
||||
"0/49/10": 5,
|
||||
"0/49/65532": 2,
|
||||
"0/49/65533": 2,
|
||||
"0/49/65528": [1, 5, 7],
|
||||
"0/49/65529": [0, 3, 4, 6, 8],
|
||||
"0/49/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/50/65532": 0,
|
||||
"0/50/65533": 1,
|
||||
"0/50/65528": [1],
|
||||
"0/50/65529": [0],
|
||||
"0/50/65531": [65528, 65529, 65531, 65532, 65533],
|
||||
"0/51/0": [
|
||||
{
|
||||
"0": "MyHome",
|
||||
"1": true,
|
||||
"2": null,
|
||||
"3": null,
|
||||
"4": "3jP5Rq6mlcw=",
|
||||
"5": [],
|
||||
"6": [
|
||||
"/akBUIsgAAB2ykzh+Z7oEA==",
|
||||
"/QANuACgAAAAAAD//gAsAw==",
|
||||
"/QANuACgAADNhSwQ0KnuNg==",
|
||||
"/oAAAAAAAADcM/lGrqaVzA=="
|
||||
],
|
||||
"7": 4
|
||||
}
|
||||
],
|
||||
"0/51/1": 9,
|
||||
"0/51/2": 11,
|
||||
"0/51/3": 0,
|
||||
"0/51/5": [],
|
||||
"0/51/6": [],
|
||||
"0/51/7": [],
|
||||
"0/51/8": false,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65533": 2,
|
||||
"0/51/65528": [2],
|
||||
"0/51/65529": [0, 1],
|
||||
"0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/52/1": 10172,
|
||||
"0/52/2": 1948,
|
||||
"0/52/65532": 0,
|
||||
"0/52/65533": 1,
|
||||
"0/52/65528": [],
|
||||
"0/52/65529": [],
|
||||
"0/52/65531": [1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/53/0": 25,
|
||||
"0/53/1": 2,
|
||||
"0/53/2": "MyHome",
|
||||
"0/53/3": 4660,
|
||||
"0/53/4": 12054125955590472924,
|
||||
"0/53/5": "QP0ADbgAoAAA",
|
||||
"0/53/6": 0,
|
||||
"0/53/7": [
|
||||
{
|
||||
"0": 12864791528929066571,
|
||||
"1": 12,
|
||||
"2": 11264,
|
||||
"3": 1787623,
|
||||
"4": 77786,
|
||||
"5": 3,
|
||||
"6": -50,
|
||||
"7": -51,
|
||||
"8": 28,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
}
|
||||
],
|
||||
"0/53/8": [
|
||||
{
|
||||
"0": 12864791528929066571,
|
||||
"1": 11264,
|
||||
"2": 11,
|
||||
"3": 0,
|
||||
"4": 0,
|
||||
"5": 3,
|
||||
"6": 0,
|
||||
"7": 12,
|
||||
"8": true,
|
||||
"9": true
|
||||
}
|
||||
],
|
||||
"0/53/9": 1775826714,
|
||||
"0/53/10": 64,
|
||||
"0/53/11": 96,
|
||||
"0/53/12": 247,
|
||||
"0/53/13": 57,
|
||||
"0/53/14": 1,
|
||||
"0/53/15": 1,
|
||||
"0/53/16": 0,
|
||||
"0/53/17": 0,
|
||||
"0/53/18": 0,
|
||||
"0/53/19": 1,
|
||||
"0/53/20": 0,
|
||||
"0/53/21": 0,
|
||||
"0/53/22": 795,
|
||||
"0/53/23": 795,
|
||||
"0/53/24": 0,
|
||||
"0/53/25": 796,
|
||||
"0/53/26": 797,
|
||||
"0/53/27": 0,
|
||||
"0/53/28": 687,
|
||||
"0/53/29": 161,
|
||||
"0/53/30": 0,
|
||||
"0/53/31": 0,
|
||||
"0/53/32": 0,
|
||||
"0/53/33": 466,
|
||||
"0/53/34": 0,
|
||||
"0/53/35": 0,
|
||||
"0/53/36": 0,
|
||||
"0/53/37": 0,
|
||||
"0/53/38": 0,
|
||||
"0/53/39": 251,
|
||||
"0/53/40": 143,
|
||||
"0/53/41": 0,
|
||||
"0/53/42": 142,
|
||||
"0/53/43": 0,
|
||||
"0/53/44": 0,
|
||||
"0/53/45": 0,
|
||||
"0/53/46": 0,
|
||||
"0/53/47": 0,
|
||||
"0/53/48": 98,
|
||||
"0/53/49": 1,
|
||||
"0/53/50": 2,
|
||||
"0/53/51": 0,
|
||||
"0/53/52": 0,
|
||||
"0/53/53": 0,
|
||||
"0/53/54": 7,
|
||||
"0/53/55": 1,
|
||||
"0/53/59": {
|
||||
"0": 672,
|
||||
"1": 143
|
||||
},
|
||||
"0/53/60": "AB//4A==",
|
||||
"0/53/61": {
|
||||
"0": true,
|
||||
"1": false,
|
||||
"2": true,
|
||||
"3": true,
|
||||
"4": true,
|
||||
"5": true,
|
||||
"6": false,
|
||||
"7": true,
|
||||
"8": true,
|
||||
"9": true,
|
||||
"10": true,
|
||||
"11": true
|
||||
},
|
||||
"0/53/62": [],
|
||||
"0/53/65532": 15,
|
||||
"0/53/65533": 3,
|
||||
"0/53/65528": [],
|
||||
"0/53/65529": [0],
|
||||
"0/53/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59,
|
||||
60, 61, 62, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/56/0": null,
|
||||
"0/56/1": 0,
|
||||
"0/56/2": 0,
|
||||
"0/56/3": null,
|
||||
"0/56/5": [
|
||||
{
|
||||
"0": 3600,
|
||||
"1": 0,
|
||||
"2": "Europe/Paris"
|
||||
}
|
||||
],
|
||||
"0/56/6": [
|
||||
{
|
||||
"0": 0,
|
||||
"1": 0,
|
||||
"2": 828061200000000
|
||||
},
|
||||
{
|
||||
"0": 3600,
|
||||
"1": 828061200000000,
|
||||
"2": 846205200000000
|
||||
}
|
||||
],
|
||||
"0/56/7": null,
|
||||
"0/56/8": 2,
|
||||
"0/56/10": 2,
|
||||
"0/56/11": 2,
|
||||
"0/56/65532": 9,
|
||||
"0/56/65533": 2,
|
||||
"0/56/65528": [3],
|
||||
"0/56/65529": [0, 1, 2, 4],
|
||||
"0/56/65531": [
|
||||
0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/60/0": 0,
|
||||
"0/60/1": null,
|
||||
"0/60/2": null,
|
||||
"0/60/65532": 1,
|
||||
"0/60/65533": 1,
|
||||
"0/60/65528": [],
|
||||
"0/60/65529": [0, 1, 2],
|
||||
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/62/0": [
|
||||
{
|
||||
"254": 1
|
||||
},
|
||||
{
|
||||
"254": 2
|
||||
},
|
||||
{
|
||||
"254": 3
|
||||
},
|
||||
{
|
||||
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRDBgkBwEkCAEwCUEEjpEdjgSj9HS+HEJVD/GpyTr4aD+5fAti/w8n4eIrPgWZGhqCV0qnqaWVnQ15JLw/y001clUJvTA0F6aotXHi6zcKNQEoARgkAgE2AwQCBAEYMAQU93OvKOKKLhOjzDp+3jm7VZEuC/MwBRRa34d1hFPuca7UFWclq9cFnlPhShgwC0DDZdbO0KEk7s3FtbyASnf25X/Rwj9BNpBNviVDFPpR2hnkqttW8rmplsec7DeAiYNDGqxt5shN8rNfJHpr9+Q2GA==",
|
||||
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEAV5qZprx2HWOKSP2iCzsI7A0CHgZVtbwsQ/y4ssETfB9z00733STIN0AfD552Vi1h6fJSeEg0/pA82bJL/y0azcKNQEpARgkAmAwBBRa34d1hFPuca7UFWclq9cFnlPhSjAFFG9oKFV1nAO5dx/+jKvq8o8oKcZbGDALQDD8OnB1NcHRxx387f9wZeFDYf32VZ3ZENQrlWBTQZqEKP+K6XjWmjTWttDEeW1kiNtB1T5ZBIaJUxVdqMuNQx8Y",
|
||||
"254": 4
|
||||
}
|
||||
],
|
||||
"0/62/1": [
|
||||
{
|
||||
"1": "BIGMMa0wfrcohBn60cI5V0xt+DIkLSV24OUKndKIXUVuzH8GGO72Yl/9IfYSPDKlK2pRWlT3J4IQD9DEiZtWK6k=",
|
||||
"2": 4937,
|
||||
"3": 3003885711,
|
||||
"4": 3179312192,
|
||||
"5": "Home",
|
||||
"254": 1
|
||||
}
|
||||
],
|
||||
"0/62/2": 5,
|
||||
"0/62/3": 4,
|
||||
"0/62/4": [
|
||||
"FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBMfk9zAkBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQCm96olCh4FdOmdpai/048NktfVtRdSntFc2qDrwkfljr0v13vTxADZ8mUF2TxEmi0EpXiYLp6rcLm7SNOdQlSgY",
|
||||
"FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=",
|
||||
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEiuu42juvSBfqPqWrV0OLnN4rePFxNq+O3ajhp0IJIJi1vE5qR9vsLcZeqBXgvO6UVKKdt7CZiR2oUEeqbmnG9TcKNQEpARgkAmAwBBTjAjvCZO2QpJyarhRj7T8yYjarAzAFFOMCO8Jk7ZCknJquFGPtPzJiNqsDGDALQE7hTxTRg92QOxwA1hK3xv8DaxvxL71r6ZHcNRzug9wNnonJ+NC84SFKvKDxwcBxHYqFdIyDiDgwJNTQIBgasmIY",
|
||||
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEbUU9qPxT8hkwnSWRhFacvs82vrsjsaZsqqvM48qn3YZZmQwtvEeyRKl6EDEzFbqd6lAdav4Sr0sunvDLgIHtrjcKNQEpARgkAmAwBBRvaChVdZwDuXcf/oyr6vKPKCnGWzAFFG9oKFV1nAO5dx/+jKvq8o8oKcZbGDALQLa3jnnqN0/o6VG8wM4V9FDzrgDfKPd5cn3BBz77K80Jzo/aNotaTNOa6zX//yIvOkBZfGyq1Dh1vXZ4g2NKcXoY"
|
||||
],
|
||||
"0/62/5": 4,
|
||||
"0/62/65532": 0,
|
||||
"0/62/65533": 1,
|
||||
"0/62/65528": [1, 3, 5, 8],
|
||||
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
|
||||
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/63/0": [],
|
||||
"0/63/1": [],
|
||||
"0/63/2": 4,
|
||||
"0/63/3": 3,
|
||||
"0/63/65532": 0,
|
||||
"0/63/65533": 2,
|
||||
"0/63/65528": [2, 5],
|
||||
"0/63/65529": [0, 1, 3, 4],
|
||||
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/70/0": 120,
|
||||
"0/70/1": 300,
|
||||
"0/70/2": 2000,
|
||||
"0/70/65532": 0,
|
||||
"0/70/65533": 3,
|
||||
"0/70/65528": [],
|
||||
"0/70/65529": [],
|
||||
"0/70/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/323615744/1": true,
|
||||
"0/323615744/65532": 0,
|
||||
"0/323615744/65533": 1,
|
||||
"0/323615744/65528": [],
|
||||
"0/323615744/65529": [],
|
||||
"0/323615744/65531": [1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/3/0": 0,
|
||||
"1/3/1": 4,
|
||||
"1/3/65532": 0,
|
||||
"1/3/65533": 5,
|
||||
"1/3/65528": [],
|
||||
"1/3/65529": [0],
|
||||
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 769,
|
||||
"1": 4
|
||||
}
|
||||
],
|
||||
"1/29/1": [3, 29, 30, 513, 516, 319486977],
|
||||
"1/29/2": [1026],
|
||||
"1/29/3": [],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 2,
|
||||
"1/29/65528": [],
|
||||
"1/29/65529": [],
|
||||
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/30/0": [],
|
||||
"1/30/65532": 0,
|
||||
"1/30/65533": 1,
|
||||
"1/30/65528": [],
|
||||
"1/30/65529": [],
|
||||
"1/30/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/513/0": 1620,
|
||||
"1/513/3": 1000,
|
||||
"1/513/4": 3000,
|
||||
"1/513/16": -15,
|
||||
"1/513/18": 1750,
|
||||
"1/513/21": 1000,
|
||||
"1/513/22": 3000,
|
||||
"1/513/26": 0,
|
||||
"1/513/27": 2,
|
||||
"1/513/28": 4,
|
||||
"1/513/35": 0,
|
||||
"1/513/36": 0,
|
||||
"1/513/41": 0,
|
||||
"1/513/48": 0,
|
||||
"1/513/49": 0,
|
||||
"1/513/50": 0,
|
||||
"1/513/72": [
|
||||
{
|
||||
"0": 1,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 2,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 3,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 4,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 5,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 6,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 254,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
}
|
||||
],
|
||||
"1/513/74": 8,
|
||||
"1/513/78": null,
|
||||
"1/513/80": [
|
||||
{
|
||||
"0": "AQ==",
|
||||
"1": 1,
|
||||
"2": "Home",
|
||||
"4": 2200,
|
||||
"5": true
|
||||
},
|
||||
{
|
||||
"0": "Ag==",
|
||||
"1": 2,
|
||||
"2": "Away",
|
||||
"4": 1800,
|
||||
"5": true
|
||||
},
|
||||
{
|
||||
"0": "Aw==",
|
||||
"1": 3,
|
||||
"2": "Sleep",
|
||||
"4": 2000,
|
||||
"5": false
|
||||
},
|
||||
{
|
||||
"0": "BA==",
|
||||
"1": 4,
|
||||
"2": "Wake",
|
||||
"4": 2300,
|
||||
"5": false
|
||||
},
|
||||
{
|
||||
"0": "BQ==",
|
||||
"1": 5,
|
||||
"2": "Vacation",
|
||||
"4": 1600,
|
||||
"5": false
|
||||
},
|
||||
{
|
||||
"0": "Bg==",
|
||||
"1": 6,
|
||||
"2": "GoingToSleep",
|
||||
"4": 2100,
|
||||
"5": false
|
||||
},
|
||||
{
|
||||
"0": "/g==",
|
||||
"1": 254,
|
||||
"2": "Eco",
|
||||
"4": 1600,
|
||||
"5": false
|
||||
}
|
||||
],
|
||||
"1/513/82": 0,
|
||||
"1/513/65532": 257,
|
||||
"1/513/65533": 8,
|
||||
"1/513/65528": [253],
|
||||
"1/513/65529": [0, 6, 254],
|
||||
"1/513/65531": [
|
||||
0, 3, 4, 16, 18, 21, 22, 26, 27, 28, 35, 36, 41, 48, 49, 50, 72, 74, 78,
|
||||
80, 82, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"1/516/0": 0,
|
||||
"1/516/1": 0,
|
||||
"1/516/2": 0,
|
||||
"1/516/65532": 0,
|
||||
"1/516/65533": 2,
|
||||
"1/516/65528": [],
|
||||
"1/516/65529": [],
|
||||
"1/516/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/319486977/319422464": "AAJ9AAsCAAADAjYoBAxGWDQ2TzFNMDM2NDecAQD/BAECAyz5AQEdAQj/BCUCVQY7DTEuMC4xNS8yLjIuMAA8AQA3AQA/AQAmAQAnAQBPBgAAICAoKP8DIwHx/wYmBCXZAiD/BDQCmAhFBQUAAAAARgkFAAAADgAAQgZJBgUMCBCAAUQRBQAABQMAAAAAAAAAAAAAAABHEQUAAAAAAAAAAAAAAAAAAAAASAYFAAAAAABKBgUAAAAAAP8LIgkQAAAAAAAAAAA=",
|
||||
"1/319486977/319422466": "bwIAAAAAAAAAAAAABgECEQIQARIBHQE0Ag0AABAAAAAAAgAAAAEBAA==",
|
||||
"1/319486977/319422467": "FQQAAAACAAAAgAAAAAAAAAAAAAAA",
|
||||
"1/319486977/319422476": 0,
|
||||
"1/319486977/319422482": 11267,
|
||||
"1/319486977/319422487": false,
|
||||
"1/319486977/319422488": 0,
|
||||
"1/319486977/319422489": 30240,
|
||||
"1/319486977/319422490": 262144,
|
||||
"1/319486977/65532": 0,
|
||||
"1/319486977/65533": 1,
|
||||
"1/319486977/65528": [],
|
||||
"1/319486977/65529": [319422464],
|
||||
"1/319486977/65531": [
|
||||
65528, 65529, 65531, 319422464, 319422465, 319422466, 319422467,
|
||||
319422468, 319422469, 319422476, 319422482, 319422487, 319422488,
|
||||
319422489, 319422490, 65532, 65533
|
||||
]
|
||||
},
|
||||
"attribute_subscriptions": []
|
||||
}
|
||||
@@ -391,7 +391,7 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_local_temperature_remote_sensing-entry]
|
||||
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_local_temperature_remote_sensing-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -404,7 +404,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_local_temperature_remote_sensing',
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_local_temperature_remote_sensing',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -426,20 +426,20 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_local_temperature_remote_sensing-state]
|
||||
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_local_temperature_remote_sensing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo Local temperature remote sensing',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Local temperature remote sensing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_local_temperature_remote_sensing',
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_local_temperature_remote_sensing',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_outdoor_temperature_remote_sensing-entry]
|
||||
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -452,7 +452,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_outdoor_temperature_remote_sensing',
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -474,13 +474,109 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_outdoor_temperature_remote_sensing-state]
|
||||
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo Outdoor temperature remote sensing',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Outdoor temperature remote sensing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_outdoor_temperature_remote_sensing',
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_local_temperature_remote_sensing-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.eve_thermo_20ecd1701_local_temperature_remote_sensing',
|
||||
'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': 'Local temperature remote sensing',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'thermostat_remote_sensing_local_temperature',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatRemoteSensing_LocalTemperature-513-26',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_local_temperature_remote_sensing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Local temperature remote sensing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ecd1701_local_temperature_remote_sensing',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing-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.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing',
|
||||
'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': 'Outdoor temperature remote sensing',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'thermostat_remote_sensing_outdoor_temperature',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatRemoteSensing_OutdoorTemperature-513-26',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Outdoor temperature remote sensing',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -1418,7 +1418,7 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[eve_thermo][button.eve_thermo_identify-entry]
|
||||
# name: test_buttons[eve_thermo_v4][button.eve_thermo_20ebp1701_identify-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -1431,7 +1431,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.eve_thermo_identify',
|
||||
'entity_id': 'button.eve_thermo_20ebp1701_identify',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -1453,14 +1453,63 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[eve_thermo][button.eve_thermo_identify-state]
|
||||
# name: test_buttons[eve_thermo_v4][button.eve_thermo_20ebp1701_identify-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'identify',
|
||||
'friendly_name': 'Eve Thermo Identify',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Identify',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.eve_thermo_identify',
|
||||
'entity_id': 'button.eve_thermo_20ebp1701_identify',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[eve_thermo_v5][button.eve_thermo_20ecd1701_identify-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.eve_thermo_20ecd1701_identify',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Identify',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-IdentifyButton-3-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[eve_thermo_v5][button.eve_thermo_20ecd1701_identify-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'identify',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Identify',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.eve_thermo_20ecd1701_identify',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_climates[eve_thermo][climate.eve_thermo-entry]
|
||||
# name: test_climates[eve_thermo_v4][climate.eve_thermo_20ebp1701-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -211,7 +211,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.eve_thermo',
|
||||
'entity_id': 'climate.eve_thermo_20ebp1701',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -233,11 +233,11 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_climates[eve_thermo][climate.eve_thermo-state]
|
||||
# name: test_climates[eve_thermo_v4][climate.eve_thermo_20ebp1701-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.0,
|
||||
'friendly_name': 'Eve Thermo',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
@@ -248,7 +248,71 @@
|
||||
'temperature': 17.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.eve_thermo',
|
||||
'entity_id': 'climate.eve_thermo_20ebp1701',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_climates[eve_thermo_v5][climate.eve_thermo_20ecd1701-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 10.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.eve_thermo_20ecd1701',
|
||||
'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': None,
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-MatterThermostat-513-0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_climates[eve_thermo_v5][climate.eve_thermo_20ecd1701-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 16.2,
|
||||
'friendly_name': 'Eve Thermo 20ECD1701',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 10.0,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'temperature': 17.5,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.eve_thermo_20ecd1701',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -115,6 +115,64 @@
|
||||
'state': '10',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_thermostat_w500][number.floor_heating_thermostat_occupied_setback-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 3.0,
|
||||
'min': 0.5,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.floor_heating_thermostat_occupied_setback',
|
||||
'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': 'Occupied setback',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'occupied_setback',
|
||||
'unique_id': '00000000000004D2-0000000000000064-MatterNodeDevice-1-ThermostatOccupiedSetback-513-52',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_thermostat_w500][number.floor_heating_thermostat_occupied_setback-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Floor Heating Thermostat Occupied setback',
|
||||
'max': 3.0,
|
||||
'min': 0.5,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.floor_heating_thermostat_occupied_setback',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_u200][number.aqara_smart_lock_u200_user_code_temporary_disable_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -979,7 +1037,7 @@
|
||||
'state': '3',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[eve_thermo][number.eve_thermo_temperature_offset-entry]
|
||||
# name: test_numbers[eve_thermo_v4][number.eve_thermo_20ebp1701_temperature_offset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -997,7 +1055,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.eve_thermo_temperature_offset',
|
||||
'entity_id': 'number.eve_thermo_20ebp1701_temperature_offset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -1019,11 +1077,11 @@
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[eve_thermo][number.eve_thermo_temperature_offset-state]
|
||||
# name: test_numbers[eve_thermo_v4][number.eve_thermo_20ebp1701_temperature_offset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Eve Thermo Temperature offset',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Temperature offset',
|
||||
'max': 50,
|
||||
'min': -50,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
@@ -1031,13 +1089,72 @@
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.eve_thermo_temperature_offset',
|
||||
'entity_id': 'number.eve_thermo_20ebp1701_temperature_offset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[eve_thermo_v5][number.eve_thermo_20ecd1701_temperature_offset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 50,
|
||||
'min': -50,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.eve_thermo_20ecd1701_temperature_offset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature offset',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_offset',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-EveTemperatureOffset-513-16',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[eve_thermo_v5][number.eve_thermo_20ecd1701_temperature_offset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Temperature offset',
|
||||
'max': 50,
|
||||
'min': -50,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.eve_thermo_20ecd1701_temperature_offset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-1.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[eve_weather_sensor][number.eve_weather_altitude_above_sea_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -1400,7 +1400,7 @@
|
||||
'state': 'previous',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[eve_thermo][select.eve_thermo_temperature_display_mode-entry]
|
||||
# name: test_selects[eve_thermo_v4][select.eve_thermo_20ebp1701_temperature_display_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -1418,7 +1418,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.eve_thermo_temperature_display_mode',
|
||||
'entity_id': 'select.eve_thermo_20ebp1701_temperature_display_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -1440,17 +1440,74 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[eve_thermo][select.eve_thermo_temperature_display_mode-state]
|
||||
# name: test_selects[eve_thermo_v4][select.eve_thermo_20ebp1701_temperature_display_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo Temperature display mode',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Temperature display mode',
|
||||
'options': list([
|
||||
'Celsius',
|
||||
'Fahrenheit',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.eve_thermo_temperature_display_mode',
|
||||
'entity_id': 'select.eve_thermo_20ebp1701_temperature_display_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Celsius',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[eve_thermo_v5][select.eve_thermo_20ecd1701_temperature_display_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Celsius',
|
||||
'Fahrenheit',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.eve_thermo_20ecd1701_temperature_display_mode',
|
||||
'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': 'Temperature display mode',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_display_mode',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[eve_thermo_v5][select.eve_thermo_20ecd1701_temperature_display_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Temperature display mode',
|
||||
'options': list([
|
||||
'Celsius',
|
||||
'Fahrenheit',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.eve_thermo_20ecd1701_temperature_display_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -4904,7 +4904,7 @@
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_battery-entry]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4919,7 +4919,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.eve_thermo_battery',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -4941,23 +4941,23 @@
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_battery-state]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Eve Thermo Battery',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Battery',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_battery',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_battery_voltage-entry]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_battery_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -4972,7 +4972,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.eve_thermo_battery_voltage',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_battery_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -5000,23 +5000,23 @@
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_battery_voltage-state]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_battery_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Eve Thermo Battery voltage',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Battery voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_battery_voltage',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_battery_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '3.05',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_temperature-entry]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -5031,7 +5031,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.eve_thermo_temperature',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -5056,23 +5056,23 @@
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_temperature-state]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Eve Thermo Temperature',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_temperature',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '21.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_valve_position-entry]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_valve_position-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -5085,7 +5085,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.eve_thermo_valve_position',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_valve_position',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -5107,20 +5107,178 @@
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo][sensor.eve_thermo_valve_position-state]
|
||||
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_valve_position-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo Valve position',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Valve position',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_valve_position',
|
||||
'entity_id': 'sensor.eve_thermo_20ebp1701_valve_position',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_battery-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.eve_thermo_20ecd1701_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': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-0-PowerSource-47-12',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Battery',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_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.eve_thermo_20ecd1701_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': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatLocalTemperature-513-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '16.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_valve_position-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': None,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_valve_position',
|
||||
'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': 'Valve position',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'valve_position',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-EveThermoValvePosition-319486977-319422488',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_valve_position-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Valve position',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_valve_position',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[eve_thermo][switch.eve_thermo_child_lock-entry]
|
||||
# name: test_switches[eve_thermo_v4][switch.eve_thermo_20ebp1701_child_lock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -500,7 +500,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.eve_thermo_child_lock',
|
||||
'entity_id': 'switch.eve_thermo_20ebp1701_child_lock',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -522,13 +522,61 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[eve_thermo][switch.eve_thermo_child_lock-state]
|
||||
# name: test_switches[eve_thermo_v4][switch.eve_thermo_20ebp1701_child_lock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo Child lock',
|
||||
'friendly_name': 'Eve Thermo 20EBP1701 Child lock',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.eve_thermo_child_lock',
|
||||
'entity_id': 'switch.eve_thermo_20ebp1701_child_lock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[eve_thermo_v5][switch.eve_thermo_20ecd1701_child_lock-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': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.eve_thermo_20ecd1701_child_lock',
|
||||
'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': 'Child lock',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'child_lock',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-EveTrvChildLock-516-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[eve_thermo_v5][switch.eve_thermo_20ecd1701_child_lock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Child lock',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.eve_thermo_20ecd1701_child_lock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -236,6 +236,50 @@ async def test_microwave_oven(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["aqara_thermostat_w500"])
|
||||
async def test_thermostat_occupied_setback(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test thermostat occupied setback number entity."""
|
||||
|
||||
entity_id = "number.floor_heating_thermostat_occupied_setback"
|
||||
|
||||
# Initial value comes from 1/513/52 with scale /10 (5 -> 0.5 °C)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "0.5"
|
||||
|
||||
# Update attribute to 30 (-> 3.0 °C)
|
||||
set_node_attribute(matter_node, 1, 513, 52, 30)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "3.0"
|
||||
|
||||
# Setting value to 2.0 °C writes 20 to OccupiedSetback (scale x10)
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"value": 2.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.write_attribute.call_count == 1
|
||||
assert matter_client.write_attribute.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
attribute_path=create_attribute_path_from_attribute(
|
||||
endpoint_id=1,
|
||||
attribute=clusters.Thermostat.Attributes.OccupiedSetback,
|
||||
),
|
||||
value=20,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["door_lock"])
|
||||
async def test_lock_attributes(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -201,7 +201,7 @@ async def test_battery_sensor_description(
|
||||
state = hass.states.get("sensor.smoke_sensor_battery_type") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo"])
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo_v4"])
|
||||
async def test_eve_thermo_sensor(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
@@ -209,26 +209,26 @@ async def test_eve_thermo_sensor(
|
||||
) -> None:
|
||||
"""Test Eve Thermo."""
|
||||
# Valve position
|
||||
state = hass.states.get("sensor.eve_thermo_valve_position")
|
||||
state = hass.states.get("sensor.eve_thermo_20ebp1701_valve_position")
|
||||
assert state
|
||||
assert state.state == "10"
|
||||
|
||||
set_node_attribute(matter_node, 1, 319486977, 319422488, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.eve_thermo_valve_position")
|
||||
state = hass.states.get("sensor.eve_thermo_20ebp1701_valve_position")
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
|
||||
# LocalTemperature
|
||||
state = hass.states.get("sensor.eve_thermo_temperature")
|
||||
state = hass.states.get("sensor.eve_thermo_20ebp1701_temperature")
|
||||
assert state
|
||||
assert state.state == "21.0"
|
||||
|
||||
set_node_attribute(matter_node, 1, 513, 0, 1800)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.eve_thermo_temperature")
|
||||
state = hass.states.get("sensor.eve_thermo_20ebp1701_temperature")
|
||||
assert state
|
||||
assert state.state == "18.0"
|
||||
|
||||
|
||||
@@ -116,32 +116,32 @@ async def test_power_switch(hass: HomeAssistant, matter_node: MatterNode) -> Non
|
||||
assert state.attributes["friendly_name"] == "Room AirConditioner Power"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo"])
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo_v4"])
|
||||
async def test_numeric_switch(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test numeric switch entity is discovered and working using an Eve Thermo fixture ."""
|
||||
state = hass.states.get("switch.eve_thermo_child_lock")
|
||||
state = hass.states.get("switch.eve_thermo_20ebp1701_child_lock")
|
||||
assert state
|
||||
assert state.state == "off"
|
||||
# name should be derived from description attribute
|
||||
assert state.attributes["friendly_name"] == "Eve Thermo Child lock"
|
||||
assert state.attributes["friendly_name"] == "Eve Thermo 20EBP1701 Child lock"
|
||||
# test attribute changes
|
||||
set_node_attribute(matter_node, 1, 516, 1, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("switch.eve_thermo_child_lock")
|
||||
state = hass.states.get("switch.eve_thermo_20ebp1701_child_lock")
|
||||
assert state.state == "on"
|
||||
set_node_attribute(matter_node, 1, 516, 1, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("switch.eve_thermo_child_lock")
|
||||
state = hass.states.get("switch.eve_thermo_20ebp1701_child_lock")
|
||||
assert state.state == "off"
|
||||
# test switch service
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
{"entity_id": "switch.eve_thermo_child_lock"},
|
||||
{"entity_id": "switch.eve_thermo_20ebp1701_child_lock"},
|
||||
blocking=True,
|
||||
)
|
||||
assert matter_client.write_attribute.call_count == 1
|
||||
@@ -156,7 +156,7 @@ async def test_numeric_switch(
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
"turn_off",
|
||||
{"entity_id": "switch.eve_thermo_child_lock"},
|
||||
{"entity_id": "switch.eve_thermo_20ebp1701_child_lock"},
|
||||
blocking=True,
|
||||
)
|
||||
assert matter_client.write_attribute.call_count == 2
|
||||
|
||||
572
tests/components/mill/test_climate.py
Normal file
572
tests/components/mill/test_climate.py
Normal file
@@ -0,0 +1,572 @@
|
||||
"""Tests for Mill climate."""
|
||||
|
||||
import contextlib
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from mill import Heater
|
||||
from mill_local import OperationMode
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import mill
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.mill.const import DOMAIN
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
HEATER_ID = "dev_id"
|
||||
HEATER_NAME = "heater_name"
|
||||
ENTITY_CLIMATE = f"climate.{HEATER_NAME}"
|
||||
|
||||
TEST_SET_TEMPERATURE = 25
|
||||
TEST_AMBIENT_TEMPERATURE = 20
|
||||
|
||||
NULL_EFFECT = nullcontext()
|
||||
|
||||
## MILL AND LOCAL MILL FIXTURES
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_mill():
|
||||
"""Mock the mill.Mill object.
|
||||
|
||||
It is imported and initialized only in /homeassistant/components/mill/__init__.py
|
||||
"""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.mill.Mill",
|
||||
autospec=True,
|
||||
) as mock_mill_class,
|
||||
):
|
||||
mill = mock_mill_class.return_value
|
||||
mill.connect.return_value = True
|
||||
mill.fetch_heater_and_sensor_data.return_value = {}
|
||||
mill.fetch_historic_energy_usage.return_value = {}
|
||||
yield mill
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_mill_local():
|
||||
"""Mock the mill_local.Mill object."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.mill.MillLocal",
|
||||
autospec=True,
|
||||
) as mock_mill_local_class,
|
||||
):
|
||||
milllocal = mock_mill_local_class.return_value
|
||||
milllocal.url = "http://dummy.url"
|
||||
milllocal.name = HEATER_NAME
|
||||
milllocal.mac_address = "dead:beef"
|
||||
milllocal.version = "0x210927"
|
||||
milllocal.connect.return_value = {
|
||||
"name": milllocal.name,
|
||||
"mac_address": milllocal.mac_address,
|
||||
"version": milllocal.version,
|
||||
"operation_key": "",
|
||||
"status": "ok",
|
||||
}
|
||||
status = {
|
||||
"ambient_temperature": TEST_AMBIENT_TEMPERATURE,
|
||||
"set_temperature": TEST_AMBIENT_TEMPERATURE,
|
||||
"current_power": 0,
|
||||
"control_signal": 0,
|
||||
"raw_ambient_temperature": TEST_AMBIENT_TEMPERATURE,
|
||||
"operation_mode": OperationMode.OFF.value,
|
||||
}
|
||||
milllocal.fetch_heater_and_sensor_data.return_value = status
|
||||
milllocal._status = status
|
||||
yield milllocal
|
||||
|
||||
|
||||
## CLOUD HEATER INTEGRATION
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cloud_heater(hass: HomeAssistant, mock_mill: MagicMock) -> Heater:
|
||||
"""Load Mill integration and creates one cloud heater."""
|
||||
|
||||
heater = Heater(
|
||||
name=HEATER_NAME,
|
||||
device_id=HEATER_ID,
|
||||
available=True,
|
||||
is_heating=False,
|
||||
power_status=False,
|
||||
current_temp=float(TEST_AMBIENT_TEMPERATURE),
|
||||
set_temp=float(TEST_AMBIENT_TEMPERATURE),
|
||||
)
|
||||
|
||||
devices = {HEATER_ID: heater}
|
||||
|
||||
mock_mill.fetch_heater_and_sensor_data.return_value = devices
|
||||
mock_mill.devices = devices
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
mill.CONF_USERNAME: "user",
|
||||
mill.CONF_PASSWORD: "pswd",
|
||||
mill.CONNECTION_TYPE: mill.CLOUD,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# We just need to load the climate component.
|
||||
with patch("homeassistant.components.mill.PLATFORMS", [Platform.CLIMATE]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return heater
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cloud_heater_set_temp(mock_mill: MagicMock, cloud_heater: MagicMock):
|
||||
"""Gets mock for the cloud heater `set_heater_temp` method."""
|
||||
return mock_mill.set_heater_temp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cloud_heater_control(mock_mill: MagicMock, cloud_heater: MagicMock):
|
||||
"""Gets mock for the cloud heater `heater_control` method."""
|
||||
return mock_mill.heater_control
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def functional_cloud_heater(
|
||||
cloud_heater: MagicMock,
|
||||
cloud_heater_set_temp: MagicMock,
|
||||
cloud_heater_control: MagicMock,
|
||||
) -> Heater:
|
||||
"""Make sure the cloud heater is "functional".
|
||||
|
||||
This will create a pseudo-functional cloud heater,
|
||||
meaning that function calls will edit the original cloud heater
|
||||
in a similar way that the API would.
|
||||
"""
|
||||
|
||||
def calculate_heating():
|
||||
if (
|
||||
cloud_heater.power_status
|
||||
and cloud_heater.set_temp > cloud_heater.current_temp
|
||||
):
|
||||
cloud_heater.is_heating = True
|
||||
|
||||
def set_temperature(device_id: str, set_temp: float):
|
||||
assert device_id == HEATER_ID, "set_temperature called with wrong device_id"
|
||||
|
||||
cloud_heater.set_temp = set_temp
|
||||
|
||||
calculate_heating()
|
||||
|
||||
def heater_control(device_id: str, power_status: bool):
|
||||
assert device_id == HEATER_ID, "set_temperature called with wrong device_id"
|
||||
|
||||
# power_status gives the "do we want to heat, Y/N", while is_heating is based on temperature and internal state and whatnot.
|
||||
cloud_heater.power_status = power_status
|
||||
|
||||
calculate_heating()
|
||||
|
||||
cloud_heater_set_temp.side_effect = set_temperature
|
||||
cloud_heater_control.side_effect = heater_control
|
||||
|
||||
return cloud_heater
|
||||
|
||||
|
||||
## LOCAL HEATER INTEGRATION
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def local_heater(hass: HomeAssistant, mock_mill_local: MagicMock) -> dict:
|
||||
"""Local Mill Heater.
|
||||
|
||||
This returns a by-reference status dict
|
||||
with which this heater's information is organised and updated.
|
||||
"""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
mill.CONF_IP_ADDRESS: "192.168.1.59",
|
||||
mill.CONNECTION_TYPE: mill.LOCAL,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# We just need to load the climate component.
|
||||
with patch("homeassistant.components.mill.PLATFORMS", [Platform.CLIMATE]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_mill_local._status
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def local_heater_set_target_temperature(
|
||||
mock_mill_local: MagicMock, local_heater: MagicMock
|
||||
):
|
||||
"""Gets mock for the local heater `set_target_temperature` method."""
|
||||
return mock_mill_local.set_target_temperature
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def local_heater_set_mode_control_individually(
|
||||
mock_mill_local: MagicMock, local_heater: MagicMock
|
||||
):
|
||||
"""Gets mock for the local heater `set_operation_mode_control_individually` method."""
|
||||
return mock_mill_local.set_operation_mode_control_individually
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def local_heater_set_mode_off(
|
||||
mock_mill_local: MagicMock, local_heater: MagicMock
|
||||
):
|
||||
"""Gets mock for the local heater `set_operation_mode_off` method."""
|
||||
return mock_mill_local.set_operation_mode_off
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def functional_local_heater(
|
||||
mock_mill_local: MagicMock,
|
||||
local_heater_set_target_temperature: MagicMock,
|
||||
local_heater_set_mode_control_individually: MagicMock,
|
||||
local_heater_set_mode_off: MagicMock,
|
||||
local_heater: MagicMock,
|
||||
) -> None:
|
||||
"""Make sure the local heater is "functional".
|
||||
|
||||
This will create a pseudo-functional local heater,
|
||||
meaning that function calls will edit the original local heater
|
||||
in a similar way that the API would.
|
||||
"""
|
||||
|
||||
def set_temperature(target_temperature: float):
|
||||
local_heater["set_temperature"] = target_temperature
|
||||
|
||||
def set_operation_mode(operation_mode: OperationMode):
|
||||
local_heater["operation_mode"] = operation_mode.value
|
||||
|
||||
def mode_control_individually():
|
||||
set_operation_mode(OperationMode.CONTROL_INDIVIDUALLY)
|
||||
|
||||
def mode_off():
|
||||
set_operation_mode(OperationMode.OFF)
|
||||
|
||||
local_heater_set_target_temperature.side_effect = set_temperature
|
||||
local_heater_set_mode_control_individually.side_effect = mode_control_individually
|
||||
local_heater_set_mode_off.side_effect = mode_off
|
||||
|
||||
|
||||
### CLOUD
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"before_state",
|
||||
"before_attrs",
|
||||
"service_name",
|
||||
"service_params",
|
||||
"effect",
|
||||
"heater_control_calls",
|
||||
"heater_set_temp_calls",
|
||||
"after_state",
|
||||
"after_attrs",
|
||||
),
|
||||
[
|
||||
# set_hvac_mode
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
NULL_EFFECT,
|
||||
[call(HEATER_ID, power_status=True)],
|
||||
[],
|
||||
HVACMode.HEAT,
|
||||
{},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
NULL_EFFECT,
|
||||
[call(HEATER_ID, power_status=False)],
|
||||
[],
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
pytest.raises(HomeAssistantError),
|
||||
[],
|
||||
[],
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
),
|
||||
# set_temperature (with hvac mode)
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
NULL_EFFECT,
|
||||
[call(HEATER_ID, power_status=True)],
|
||||
[call(HEATER_ID, float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.HEAT,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
NULL_EFFECT,
|
||||
[call(HEATER_ID, power_status=False)],
|
||||
[call(HEATER_ID, float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
NULL_EFFECT,
|
||||
[],
|
||||
[call(HEATER_ID, float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
pytest.raises(HomeAssistantError),
|
||||
# MillHeater will set the temperature before calling async_handle_set_hvac_mode,
|
||||
# meaning an invalid HVAC mode will raise only after the temperature is set.
|
||||
[],
|
||||
[call(HEATER_ID, float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
# likewise, in this test, it hasn't had the chance to update its ambient temperature,
|
||||
# because the exception is raised before a refresh can be requested from the coordinator
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_cloud_heater(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
functional_cloud_heater: MagicMock,
|
||||
cloud_heater_control: MagicMock,
|
||||
cloud_heater_set_temp: MagicMock,
|
||||
before_state: HVACMode,
|
||||
before_attrs: dict,
|
||||
service_name: str,
|
||||
service_params: dict,
|
||||
effect: "contextlib.AbstractContextManager",
|
||||
heater_control_calls: list,
|
||||
heater_set_temp_calls: list,
|
||||
after_state: HVACMode,
|
||||
after_attrs: dict,
|
||||
) -> None:
|
||||
"""Tests setting HVAC mode (directly or through set_temperature) for a cloud heater."""
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state is not None
|
||||
assert state.state == before_state
|
||||
for attr, value in before_attrs.items():
|
||||
assert state.attributes.get(attr) == value
|
||||
|
||||
with effect:
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
service_name,
|
||||
service_params | {ATTR_ENTITY_ID: ENTITY_CLIMATE},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cloud_heater_control.assert_has_calls(heater_control_calls)
|
||||
cloud_heater_set_temp.assert_has_calls(heater_set_temp_calls)
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state is not None
|
||||
assert state.state == after_state
|
||||
for attr, value in after_attrs.items():
|
||||
assert state.attributes.get(attr) == value
|
||||
|
||||
|
||||
### LOCAL
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"before_state",
|
||||
"before_attrs",
|
||||
"service_name",
|
||||
"service_params",
|
||||
"effect",
|
||||
"heater_mode_set_individually_calls",
|
||||
"heater_mode_set_off_calls",
|
||||
"heater_set_target_temperature_calls",
|
||||
"after_state",
|
||||
"after_attrs",
|
||||
),
|
||||
[
|
||||
# set_hvac_mode
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
NULL_EFFECT,
|
||||
[call()],
|
||||
[],
|
||||
[],
|
||||
HVACMode.HEAT,
|
||||
{},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
NULL_EFFECT,
|
||||
[],
|
||||
[call()],
|
||||
[],
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
pytest.raises(HomeAssistantError),
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
HVACMode.OFF,
|
||||
{},
|
||||
),
|
||||
# set_temperature (with hvac mode)
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
NULL_EFFECT,
|
||||
[call()],
|
||||
[],
|
||||
[call(float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.HEAT,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
NULL_EFFECT,
|
||||
[],
|
||||
[call()],
|
||||
[call(float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
NULL_EFFECT,
|
||||
[],
|
||||
[],
|
||||
[call(float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE},
|
||||
),
|
||||
(
|
||||
HVACMode.OFF,
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: TEST_SET_TEMPERATURE, ATTR_HVAC_MODE: HVACMode.COOL},
|
||||
pytest.raises(HomeAssistantError),
|
||||
# LocalMillHeater will set the temperature before calling async_handle_set_hvac_mode,
|
||||
# meaning an invalid HVAC mode will raise only after the temperature is set.
|
||||
[],
|
||||
[],
|
||||
[call(float(TEST_SET_TEMPERATURE))],
|
||||
HVACMode.OFF,
|
||||
# likewise, in this test, it hasn't had the chance to update its ambient temperature,
|
||||
# because the exception is raised before a refresh can be requested from the coordinator
|
||||
{ATTR_TEMPERATURE: TEST_AMBIENT_TEMPERATURE},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_local_heater(
|
||||
hass: HomeAssistant,
|
||||
functional_local_heater: MagicMock,
|
||||
local_heater_set_mode_control_individually: MagicMock,
|
||||
local_heater_set_mode_off: MagicMock,
|
||||
local_heater_set_target_temperature: MagicMock,
|
||||
before_state: HVACMode,
|
||||
before_attrs: dict,
|
||||
service_name: str,
|
||||
service_params: dict,
|
||||
effect: "contextlib.AbstractContextManager",
|
||||
heater_mode_set_individually_calls: list,
|
||||
heater_mode_set_off_calls: list,
|
||||
heater_set_target_temperature_calls: list,
|
||||
after_state: HVACMode,
|
||||
after_attrs: dict,
|
||||
) -> None:
|
||||
"""Tests setting HVAC mode (directly or through set_temperature) for a local heater."""
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state is not None
|
||||
assert state.state == before_state
|
||||
for attr, value in before_attrs.items():
|
||||
assert state.attributes.get(attr) == value
|
||||
|
||||
with effect:
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
service_name,
|
||||
service_params | {ATTR_ENTITY_ID: ENTITY_CLIMATE},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
local_heater_set_mode_control_individually.assert_has_calls(
|
||||
heater_mode_set_individually_calls
|
||||
)
|
||||
local_heater_set_mode_off.assert_has_calls(heater_mode_set_off_calls)
|
||||
local_heater_set_target_temperature.assert_has_calls(
|
||||
heater_set_target_temperature_calls
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state is not None
|
||||
assert state.state == after_state
|
||||
for attr, value in after_attrs.items():
|
||||
assert state.attributes.get(attr) == value
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Tests for the Nina integration."""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.nina.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
load_json_array_fixture,
|
||||
load_json_object_fixture,
|
||||
)
|
||||
|
||||
|
||||
async def setup_platform(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
@@ -24,20 +28,20 @@ async def setup_platform(hass: HomeAssistant, config_entry: MockConfigEntry) ->
|
||||
|
||||
def mocked_request_function(url: str) -> dict[str, Any]:
|
||||
"""Mock of the request function."""
|
||||
dummy_response: dict[str, Any] = json.loads(
|
||||
load_fixture("sample_warnings.json", "nina")
|
||||
dummy_response: list[dict[str, Any]] = load_json_array_fixture(
|
||||
"sample_warnings.json", DOMAIN
|
||||
)
|
||||
|
||||
dummy_response_details: dict[str, Any] = json.loads(
|
||||
load_fixture("sample_warning_details.json", "nina")
|
||||
dummy_response_details: dict[str, Any] = load_json_object_fixture(
|
||||
"sample_warning_details.json", DOMAIN
|
||||
)
|
||||
|
||||
dummy_response_regions: dict[str, Any] = json.loads(
|
||||
load_fixture("sample_regions.json", "nina")
|
||||
dummy_response_regions: dict[str, Any] = load_json_object_fixture(
|
||||
"sample_regions.json", DOMAIN
|
||||
)
|
||||
|
||||
dummy_response_labels: dict[str, Any] = json.loads(
|
||||
load_fixture("sample_labels.json", "nina")
|
||||
dummy_response_labels: dict[str, Any] = load_json_object_fixture(
|
||||
"sample_labels.json", DOMAIN
|
||||
)
|
||||
|
||||
if "https://warnung.bund.de/api31/dashboard/" in url: # codespell:ignore bund
|
||||
|
||||
@@ -260,4 +260,12 @@ MOCK_OWPROXY_DEVICES = {
|
||||
"/EDS0066/pressure": [b" 1012.21"],
|
||||
},
|
||||
},
|
||||
"7E.333333333333": {
|
||||
ATTR_INJECT_READS: {
|
||||
"/type": [b"EDS"],
|
||||
"/device_type": [b"EDS0065"],
|
||||
"/EDS0065/temperature": [b" 13.9375"],
|
||||
"/EDS0065/humidity": [b" 41.375"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -588,6 +588,37 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_registry[7E.333333333333-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'onewire',
|
||||
'7E.333333333333',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Embedded Data Systems',
|
||||
'model': None,
|
||||
'model_id': 'EDS0065',
|
||||
'name': '7E.333333333333',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '333333333333',
|
||||
'sw_version': '3.2',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_registry[A6.111111111111-entry]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
||||
@@ -2245,6 +2245,117 @@
|
||||
'state': '13.9375',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.7e_333333333333_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.7e_333333333333_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': 'onewire',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '/7E.333333333333/EDS0065/humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.7e_333333333333_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'device_file': '/7E.333333333333/EDS0065/humidity',
|
||||
'friendly_name': '7E.333333333333 Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.7e_333333333333_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '41.375',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.7e_333333333333_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.7e_333333333333_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': 'onewire',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '/7E.333333333333/EDS0065/temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.7e_333333333333_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'device_file': '/7E.333333333333/EDS0065/temperature',
|
||||
'friendly_name': '7E.333333333333 Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.7e_333333333333_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13.9375',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.a6_111111111111_hih3600_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user