Compare commits

...

38 Commits

Author SHA1 Message Date
Erik
26874335ca Improve comments in trigger and condition test helpers 2026-01-13 08:29:04 +01:00
Erik Montnemery
a6221d16b6 Add helper for creating entity condition tests (#160425) 2026-01-13 08:25:41 +01:00
tronikos
51701cab7c Bump opower to 0.16.2 (#160822) 2026-01-12 19:20:06 -08:00
Raphael Hehl
010e1f2d0d Bump uiprotect to 8.1.1 (#160816) 2026-01-12 23:06:50 +01:00
Jonathan de Jong
66909fc9ca Support HVAC mode in set temperature calls in Mill (#155416)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-01-12 21:46:20 +01:00
Lukas
90a28c95c8 Bump python-pooldose to 0.8.2 (#160800) 2026-01-12 20:20:33 +01:00
Erik Montnemery
83f2c53e8c Disable pyright type checking in VS Code (#160528) 2026-01-12 20:19:19 +01:00
Ludovic BOUÉ
514b6e243c Rename Matter Eve Thermostat Fixture to eve_thermo_v4 (#160796) 2026-01-12 20:16:13 +01:00
Krisjanis Lejejs
742230c7be Bump hass-nabucasa from 1.8.0 to 1.9.0 (#160788) 2026-01-12 19:50:48 +01:00
Ludovic BOUÉ
acb6b1444e Add fixture for Matter Eve Thermo 20ECD1701 (v5) with detailed attributes (#160795) 2026-01-12 18:52:18 +01:00
Erwin Douna
f358b2231a Add match case in perform action (#160150) 2026-01-12 18:25:51 +01:00
Joakim Sørensen
fd24cffa6b Block untill done while setting up cloud in tests (#160780) 2026-01-12 17:32:06 +01:00
Yuxin Wang
0b5d6ee538 Add TIMESTAMP device classes to corresponding sensors in APCUPSD (#160577) 2026-01-12 17:10:25 +01:00
DeerMaximum
d125bb88d1 Use load_json_object_fixture in tests for NINA (#160690)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-12 17:09:18 +01:00
Ludovic BOUÉ
2ab51f582a Add Matter occupied setback for thermostats (#155439) 2026-01-12 16:47:43 +01:00
epenet
f9b32811b2 Move typed ConfigEntry to coordinator module in point (#160786) 2026-01-12 16:34:38 +01:00
seppwabala
41a423e140 Add support for eds0065 in onewire (#160094)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-12 16:21:00 +01:00
Xiangxuan Qu
f717867657 Pass config_entry explicitly to Point coordinator (#160578) 2026-01-12 15:55:41 +01:00
J. Nick Koston
ab202a03db Handle deleted issue during repair flow translation check (#160698) 2026-01-12 15:52:36 +01:00
Álvaro Fernández Rojas
46a3e5e5b5 Fix Airzone Q-Adapt select entities (#160695)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2026-01-12 15:48:07 +01:00
Krisjanis Lejejs
0163a4d289 Bump hass-nabucasa from 1.7.0 to 1.8.0 (#160775) 2026-01-12 15:46:49 +01:00
Willem-Jan van Rootselaar
6c1bf31a3c Bump python-bsblan to version 4.1.0 (#160676) 2026-01-12 15:44:03 +01:00
Michael
a434760a80 Complete entity name and icon translations in FRITZ!Box Tools (#160746)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 15:43:28 +01:00
Jevgeni Kiski
798990fadc Bump vallox-websocket-api to 6.0.0 (#160742) 2026-01-12 15:30:17 +01:00
Glenn de Haan
b3d9d92e4a Add HDFury diagnostics (#160641) 2026-01-12 15:08:19 +01:00
Lukas
1082a9ca69 Pooldose: Sync with docs update (#160190) 2026-01-12 14:41:46 +01:00
Joost Lekkerkerker
c247f56658 Fix fitbit icon (#160750) 2026-01-12 11:08:59 +01:00
Paul Tarjan
e7f71781f1 Fix Hikvision NVR binary sensors not being detected (#160254)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:04:30 +01:00
Josef Zweck
c4b2c5e621 Fix missing key for brew by weight in lamarzocco (#160722) 2026-01-12 11:03:36 +01:00
Thomas55555
7779609a76 Add more pollutants to Google Air Quality (#160738) 2026-01-12 11:02:18 +01:00
Duco Sebel
7b9a5f897c Bump python-homewizard-energy to 10.0.1 (#160736) 2026-01-12 10:59:55 +01:00
epenet
6eccbfc1cf Fix Requirement parsing in RequirementsManager (#160485) 2026-01-12 10:55:39 +01:00
Artur Pragacz
0da518e951 Fix scrape sensor device name (#160765) 2026-01-12 10:53:25 +01:00
Bram Kragten
e5851b7920 Update frontend to 20260107.1 (#160644) 2026-01-12 10:51:49 +01:00
Artur Pragacz
1b9364e8b5 Assign device_entry earlier in entity platform (#160767) 2026-01-12 10:49:01 +01:00
Carter Green
8460d4f5e2 Yolink diagnostic sensors (#160749) 2026-01-12 10:33:49 +01:00
Artur Pragacz
8fd35cd70d Rename registry imports in entity platform (#160766) 2026-01-12 10:27:03 +01:00
MarkGodwin
88be115699 Bump tplink_omada quality scale to bronze (#160762) 2026-01-12 09:52:46 +01:00
101 changed files with 3232 additions and 390 deletions

View File

@@ -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,

View File

@@ -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",
},

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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}",
)

View File

@@ -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*",

View File

@@ -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,

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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."""

View File

@@ -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": {

View File

@@ -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": [
{

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260107.0"]
"requirements": ["home-assistant-frontend==20260107.1"]
}

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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%]"
},

View 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,
}

View File

@@ -43,7 +43,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo

View File

@@ -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)

View File

@@ -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."]
}

View File

@@ -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
),
),
)

View File

@@ -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
),
),
)

View File

@@ -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(

View File

@@ -217,6 +217,9 @@
"led_indicator_intensity_on": {
"name": "LED on intensity"
},
"occupied_setback": {
"name": "Occupied setback"
},
"off_transition_time": {
"name": "Off transition time"
},

View File

@@ -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:

View File

@@ -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

View File

@@ -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"),
}

View File

@@ -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",

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.16.1"]
"requirements": ["opower==0.16.2"]
}

View File

@@ -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()

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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] = {}

View File

@@ -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__)

View File

@@ -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"]
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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."""

View File

@@ -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"]
}

View File

@@ -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",

View File

@@ -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"]
}

View File

@@ -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"),

View File

@@ -67,6 +67,9 @@
"current_power": {
"name": "Current power"
},
"device_temperature": {
"name": "Device temperature"
},
"power_consumption": {
"name": "Power consumption"
},

View File

@@ -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

View File

@@ -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

View File

@@ -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
),

View File

@@ -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:

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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."""

View File

@@ -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",
}

View File

@@ -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',
})
# ---

View File

@@ -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',
})
# ---

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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"]}

View File

@@ -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"

View File

@@ -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"},
)

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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'>,
}),

View File

@@ -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',

View File

@@ -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',

View File

@@ -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"
}
}
]
}

View File

@@ -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({

View 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',
}),
})
# ---

View 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

View File

@@ -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"]
)

View File

@@ -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",

View File

@@ -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**",

View 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": []
}

View File

@@ -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>,

View File

@@ -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>,

View File

@@ -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>,

View File

@@ -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({

View File

@@ -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>,

View File

@@ -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({

View File

@@ -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>,

View File

@@ -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,

View File

@@ -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"

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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"],
},
},
}

View File

@@ -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,

View File

@@ -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