Compare commits

...

45 Commits

Author SHA1 Message Date
Franck Nijhof 00627b82e0 2025.5.1 (#144564) 2025-05-09 17:03:40 +02:00
Franck Nijhof 13aba6201e Bump version to 2025.5.1 2025-05-09 13:29:29 +00:00
starkillerOG f392e0c1c7 Prevent errors during cleaning of connections/identifiers in device registry (#144558) 2025-05-09 13:28:33 +00:00
starkillerOG 181eca6c82 Reolink clean device registry mac (#144554) 2025-05-09 13:28:32 +00:00
Bram Kragten 196d923ac6 Update frontend to 20250509.0 (#144549) 2025-05-09 13:28:30 +00:00
Josef Zweck 4ad387c967 Fix statistics coordinator subscription for lamarzocco (#144541) 2025-05-09 13:28:29 +00:00
J. Nick Koston cb475bf153 Bump aiodns to 3.4.0 (#144511) 2025-05-09 13:28:28 +00:00
Michael 47acceea08 Fix removing of smarthome templates on startup of AVM Fritz!SmartHome integration (#144506) 2025-05-09 13:28:26 +00:00
J. Nick Koston fd6fb7e3bc Bump forecast-solar to 4.2.0 (#144502) 2025-05-09 13:28:25 +00:00
Erik Montnemery 30f7e9b441 Don't encrypt or decrypt unknown files in backup archives (#144495) 2025-05-09 13:28:24 +00:00
Matthias Alphart a8beec2691 Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue (#144463) 2025-05-09 13:28:23 +00:00
Fredrik Erlandsson 23244fb79f Fix point import error (#144462)
* fix import error

* fix failing tests

* Apply suggestions from code review

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-09 13:28:22 +00:00
Martin Hjelmare e5c56629e2 Fix Z-Wave reset accumulated values button entity category (#144459) 2025-05-09 13:28:20 +00:00
Josef Zweck a793503c8a Bump pylamarzocco to 2.0.1 (#144454) 2025-05-09 13:28:19 +00:00
DukeChocula 054c7a0adc Add LAP-V102S-AUSR to VeSync (#144437)
Update const.py

Added LAP-V102S-AUSR to Vital 100S
2025-05-09 13:28:18 +00:00
Tamer Wahba 6eb2d1aa7c fix homekit air purifier temperature sensor to convert unit (#144435) 2025-05-09 13:28:16 +00:00
Martin Hjelmare 619fdea5df Fix Z-Wave restore nvm command to wait for driver ready (#144413) 2025-05-09 13:28:15 +00:00
Franck Nijhof e8bdc7286e 2025.5.0 (#144406) 2025-05-07 19:13:53 +02:00
Franck Nijhof 18f2b120ef Bump version to 2025.5.0 2025-05-07 14:31:26 +00:00
Michael Hansen 43d8345821 Bump intents to 2025.5.7 (#144404) 2025-05-07 14:30:48 +00:00
Franck Nijhof 999e930fc8 Bump version to 2025.5.0b10 2025-05-07 13:04:15 +00:00
Petar Petrov d4e99efc46 Add more missing device_class translations for template helper (#144399) 2025-05-07 13:04:08 +00:00
Bram Kragten fb01a0a9f1 Update frontend to 20250507.0 (#144398) 2025-05-07 13:04:07 +00:00
Robert Resch 9556285c59 Bump deebot-client to 13.1.0 (#144397) 2025-05-07 13:04:06 +00:00
Guido Schmitz 2d40b1ec75 Bump devolo_home_control_api to 0.19.0 (#144374) 2025-05-07 13:04:04 +00:00
Thomas55555 7eb690b125 Improve activity logic in Husqvarna Automower (#144057)
* Improve activity logic in Husqvarna Automower

* add test
2025-05-07 13:04:03 +00:00
Thomas55555 a23644debc Fix test in Husqvarna Automower (#144055) 2025-05-07 13:04:02 +00:00
Franck Nijhof c98ba7f6ba Bump version to 2025.5.0b9 2025-05-07 11:09:32 +00:00
Joost Lekkerkerker aa2b61f133 Fix variables in MELCloud (#144396) 2025-05-07 11:09:07 +00:00
Joost Lekkerkerker f85d4afe45 Set SmartThings power energy state class to Total (#144395) 2025-05-07 11:09:06 +00:00
Joost Lekkerkerker b4ab9177b8 Bump pySmartThings to 3.2.1 (#144393) 2025-05-07 11:09:05 +00:00
Petar Petrov e7c310ca58 Add missing device_class translations for template helper (#144392) 2025-05-07 11:09:04 +00:00
Joost Lekkerkerker 85a83f2553 Fix SmartThings machine operating state with no options (#144390) 2025-05-07 11:09:02 +00:00
Martin Hjelmare d2e7baeb38 Fix Z-Wave controller hard reset (#144389) 2025-05-07 11:09:01 +00:00
Barry vd. Heuvel 07b2ce28b1 Bump wh-python to 2025.4.29 for Weheat integration (#144384) 2025-05-07 11:09:00 +00:00
Franck Nijhof 35c90d9bde Bump version to 2025.5.0b8 2025-05-07 07:38:18 +00:00
Raphael Hehl a9632bd0ff Bump uiprotect to version 7.6.0 (#144369) 2025-05-07 07:38:12 +00:00
epenet 983e134ae9 Bump renault-api to 0.3.1 (#144366)
* Bump renault-api to 0.3.1

* Adjust tests
2025-05-07 07:38:10 +00:00
Jan Bouwhuis e217532f9e Fix field validation for mqtt subentry options in sections (#144355) 2025-05-07 07:38:09 +00:00
Franck Nijhof 1eeab28eec Bump version to 2025.5.0b7 2025-05-06 19:30:08 +00:00
Bram Kragten 2a3bd45901 Update frontend to 20250506.0 (#144354) 2025-05-06 19:29:59 +00:00
Paulus Schoutsen d16453a465 Remove some media player intent checks for when paused (#144351) 2025-05-06 19:29:59 +00:00
Jan Bouwhuis de63dddc96 Ensure all default MQTT subentry option values are saved (#144347)
* Ensure all default MQTT subentry option values are saved

* Apply correct filter
2025-05-06 19:29:58 +00:00
J. Nick Koston ccffe19611 Bump bluemaestro-ble to 0.4.1 (#144345)
changelog: https://github.com/Bluetooth-Devices/bluemaestro-ble/compare/v0.4.0...v0.4.1

fixes #https://github.com/home-assistant/core/issues/144339
2025-05-06 19:29:57 +00:00
Martin Hjelmare 806bcf47d9 Fix Z-Wave migration flow to unload config entry before unplugging controller (#144343)
* Fix Z-Wave migration unload config entry before unplugging controller

* Remove typo
2025-05-06 19:29:56 +00:00
67 changed files with 1295 additions and 1054 deletions
+13 -3
View File
@@ -22,7 +22,7 @@ from . import util
from .agent import BackupAgent
from .const import DATA_MANAGER
from .manager import BackupManager
from .models import BackupNotFound
from .models import AgentBackup, BackupNotFound
@callback
@@ -85,7 +85,15 @@ class DownloadBackupView(HomeAssistantView):
request, headers, backup_id, agent_id, agent, manager
)
return await self._send_backup_with_password(
hass, request, headers, backup_id, agent_id, password, agent, manager
hass,
backup,
request,
headers,
backup_id,
agent_id,
password,
agent,
manager,
)
except BackupNotFound:
return Response(status=HTTPStatus.NOT_FOUND)
@@ -116,6 +124,7 @@ class DownloadBackupView(HomeAssistantView):
async def _send_backup_with_password(
self,
hass: HomeAssistant,
backup: AgentBackup,
request: Request,
headers: dict[istr, str],
backup_id: str,
@@ -144,7 +153,8 @@ class DownloadBackupView(HomeAssistantView):
stream = util.AsyncIteratorWriter(hass)
worker = threading.Thread(
target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []]
target=util.decrypt_backup,
args=[backup, reader, stream, password, on_done, 0, []],
)
try:
worker.start()
+68 -24
View File
@@ -295,13 +295,26 @@ def validate_password_stream(
raise BackupEmpty
def _get_expected_archives(backup: AgentBackup) -> set[str]:
"""Get the expected archives in the backup."""
expected_archives = set()
if backup.homeassistant_included:
expected_archives.add("homeassistant")
for addon in backup.addons:
expected_archives.add(addon.slug)
for folder in backup.folders:
expected_archives.add(folder.value)
return expected_archives
def decrypt_backup(
backup: AgentBackup,
input_stream: IO[bytes],
output_stream: IO[bytes],
password: str | None,
on_done: Callable[[Exception | None], None],
minimum_size: int,
nonces: list[bytes],
nonces: NonceGenerator,
) -> None:
"""Decrypt a backup."""
error: Exception | None = None
@@ -315,7 +328,7 @@ def decrypt_backup(
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
) as output_tar,
):
_decrypt_backup(input_tar, output_tar, password)
_decrypt_backup(backup, input_tar, output_tar, password)
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
@@ -333,15 +346,18 @@ def decrypt_backup(
def _decrypt_backup(
backup: AgentBackup,
input_tar: tarfile.TarFile,
output_tar: tarfile.TarFile,
password: str | None,
) -> None:
"""Decrypt a backup."""
expected_archives = _get_expected_archives(backup)
for obj in input_tar:
# We compare with PurePath to avoid issues with different path separators,
# for example when backup.json is added as "./backup.json"
if PurePath(obj.name) == PurePath("backup.json"):
object_path = PurePath(obj.name)
if object_path == PurePath("backup.json"):
# Rewrite the backup.json file to indicate that the backup is decrypted
if not (reader := input_tar.extractfile(obj)):
raise DecryptError
@@ -352,7 +368,13 @@ def _decrypt_backup(
metadata_obj.size = len(updated_metadata_b)
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
continue
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
prefix, _, suffix = object_path.name.partition(".")
if suffix not in ("tar", "tgz", "tar.gz"):
LOGGER.debug("Unknown file %s will not be decrypted", obj.name)
output_tar.addfile(obj, input_tar.extractfile(obj))
continue
if prefix not in expected_archives:
LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name)
output_tar.addfile(obj, input_tar.extractfile(obj))
continue
istf = SecureTarFile(
@@ -371,12 +393,13 @@ def _decrypt_backup(
def encrypt_backup(
backup: AgentBackup,
input_stream: IO[bytes],
output_stream: IO[bytes],
password: str | None,
on_done: Callable[[Exception | None], None],
minimum_size: int,
nonces: list[bytes],
nonces: NonceGenerator,
) -> None:
"""Encrypt a backup."""
error: Exception | None = None
@@ -390,7 +413,7 @@ def encrypt_backup(
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
) as output_tar,
):
_encrypt_backup(input_tar, output_tar, password, nonces)
_encrypt_backup(backup, input_tar, output_tar, password, nonces)
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
@@ -408,17 +431,20 @@ def encrypt_backup(
def _encrypt_backup(
backup: AgentBackup,
input_tar: tarfile.TarFile,
output_tar: tarfile.TarFile,
password: str | None,
nonces: list[bytes],
nonces: NonceGenerator,
) -> None:
"""Encrypt a backup."""
inner_tar_idx = 0
expected_archives = _get_expected_archives(backup)
for obj in input_tar:
# We compare with PurePath to avoid issues with different path separators,
# for example when backup.json is added as "./backup.json"
if PurePath(obj.name) == PurePath("backup.json"):
object_path = PurePath(obj.name)
if object_path == PurePath("backup.json"):
# Rewrite the backup.json file to indicate that the backup is encrypted
if not (reader := input_tar.extractfile(obj)):
raise EncryptError
@@ -429,16 +455,21 @@ def _encrypt_backup(
metadata_obj.size = len(updated_metadata_b)
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
continue
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
prefix, _, suffix = object_path.name.partition(".")
if suffix not in ("tar", "tgz", "tar.gz"):
LOGGER.debug("Unknown file %s will not be encrypted", obj.name)
output_tar.addfile(obj, input_tar.extractfile(obj))
continue
if prefix not in expected_archives:
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
continue
istf = SecureTarFile(
None, # Not used
gzip=False,
key=password_to_key(password) if password is not None else None,
mode="r",
fileobj=input_tar.extractfile(obj),
nonce=nonces[inner_tar_idx],
nonce=nonces.get(inner_tar_idx),
)
inner_tar_idx += 1
with istf.encrypt(obj) as encrypted:
@@ -456,17 +487,33 @@ class _CipherWorkerStatus:
writer: AsyncIteratorWriter
class NonceGenerator:
"""Generate nonces for encryption."""
def __init__(self) -> None:
"""Initialize the generator."""
self._nonces: dict[int, bytes] = {}
def get(self, index: int) -> bytes:
"""Get a nonce for the given index."""
if index not in self._nonces:
# Generate a new nonce for the given index
self._nonces[index] = os.urandom(16)
return self._nonces[index]
class _CipherBackupStreamer:
"""Encrypt or decrypt a backup."""
_cipher_func: Callable[
[
AgentBackup,
IO[bytes],
IO[bytes],
str | None,
Callable[[Exception | None], None],
int,
list[bytes],
NonceGenerator,
],
None,
]
@@ -484,7 +531,7 @@ class _CipherBackupStreamer:
self._hass = hass
self._open_stream = open_stream
self._password = password
self._nonces: list[bytes] = []
self._nonces = NonceGenerator()
def size(self) -> int:
"""Return the maximum size of the decrypted or encrypted backup."""
@@ -508,7 +555,15 @@ class _CipherBackupStreamer:
writer = AsyncIteratorWriter(self._hass)
worker = threading.Thread(
target=self._cipher_func,
args=[reader, writer, self._password, on_done, self.size(), self._nonces],
args=[
self._backup,
reader,
writer,
self._password,
on_done,
self.size(),
self._nonces,
],
)
worker_status = _CipherWorkerStatus(
done=asyncio.Event(), reader=reader, thread=worker, writer=writer
@@ -538,17 +593,6 @@ class DecryptedBackupStreamer(_CipherBackupStreamer):
class EncryptedBackupStreamer(_CipherBackupStreamer):
"""Encrypt a backup."""
def __init__(
self,
hass: HomeAssistant,
backup: AgentBackup,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
password: str | None,
) -> None:
"""Initialize."""
super().__init__(hass, backup, open_stream, password)
self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())]
_cipher_func = staticmethod(encrypt_backup)
def backup(self) -> AgentBackup:
@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
"iot_class": "local_push",
"requirements": ["bluemaestro-ble==0.4.0"]
"requirements": ["bluemaestro-ble==0.4.1"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.4.30"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
}
@@ -8,6 +8,6 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["devolo_home_control_api"],
"requirements": ["devolo-home-control-api==0.18.3"],
"requirements": ["devolo-home-control-api==0.19.0"],
"zeroconf": ["_dvl-deviceapi._tcp.local."]
}
@@ -68,7 +68,7 @@ async def async_validate_hostname(
result = False
with contextlib.suppress(DNSError):
result = bool(
await aiodns.DNSResolver(
await aiodns.DNSResolver( # type: ignore[call-overload]
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
)
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.3.0"]
"requirements": ["aiodns==3.4.0"]
}
+1 -1
View File
@@ -106,7 +106,7 @@ class WanIpSensor(SensorEntity):
async def async_update(self) -> None:
"""Get the current DNS IP address for hostname."""
try:
response = await self.resolver.query(self.hostname, self.querytype)
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
response = None
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"]
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["forecast-solar==4.1.0"]
"requirements": ["forecast-solar==4.2.0"]
}
@@ -92,7 +92,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
available_main_ains = [
ain
for ain, dev in data.devices.items()
for ain, dev in data.devices.items() | data.templates.items()
if dev.device_and_unit_id[1] is None
]
device_reg = dr.async_get(self.hass)
+9 -1
View File
@@ -45,7 +45,15 @@ type FroniusConfigEntry = ConfigEntry[FroniusSolarNet]
async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
"""Set up fronius from a config entry."""
host = entry.data[CONF_HOST]
fronius = Fronius(async_get_clientsession(hass), host)
fronius = Fronius(
async_get_clientsession(
hass,
# Fronius Gen24 firmware 1.35.4-1 redirects to HTTPS with self-signed
# certificate. See https://github.com/home-assistant/core/issues/138881
verify_ssl=False,
),
host,
)
solar_net = FroniusSolarNet(hass, entry, fronius)
await solar_net.init_devices()
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250502.1"]
"requirements": ["home-assistant-frontend==20250509.0"]
}
@@ -8,7 +8,13 @@ from pyhap.const import CATEGORY_AIR_PURIFIER
from pyhap.service import Service
from pyhap.util import callback as pyhap_callback
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import (
Event,
EventStateChangedData,
@@ -43,7 +49,12 @@ from .const import (
THRESHOLD_FILTER_CHANGE_NEEDED,
)
from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan
from .util import cleanup_name_for_homekit, convert_to_float, density_to_air_quality
from .util import (
cleanup_name_for_homekit,
convert_to_float,
density_to_air_quality,
temperature_to_homekit,
)
_LOGGER = logging.getLogger(__name__)
@@ -345,8 +356,13 @@ class AirPurifier(Fan):
):
return
unit = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS
)
current_temperature = temperature_to_homekit(current_temperature, unit)
_LOGGER.debug(
"%s: Linked temperature sensor %s changed to %d",
"%s: Linked temperature sensor %s changed to %d °C",
self.entity_id,
self.linked_temperature_sensor,
current_temperature,
@@ -110,10 +110,10 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
mower_attributes = self.mower_attributes
if mower_attributes.mower.state in PAUSED_STATES:
return LawnMowerActivity.PAUSED
if mower_attributes.mower.activity in MOWING_ACTIVITIES:
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
return LawnMowerActivity.RETURNING
return LawnMowerActivity.MOWING
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
return LawnMowerActivity.RETURNING
if (mower_attributes.mower.state == "RESTRICTED") or (
mower_attributes.mower.activity in DOCKED_ACTIVITIES
):
@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.0"]
"requirements": ["pylamarzocco==2.0.1"]
}
@@ -132,17 +132,18 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensor entities."""
coordinator = entry.runtime_data.config_coordinator
config_coordinator = entry.runtime_data.config_coordinator
statistic_coordinators = entry.runtime_data.statistics_coordinator
entities = [
LaMarzoccoSensorEntity(coordinator, description)
LaMarzoccoSensorEntity(config_coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
if description.supported_fn(config_coordinator)
]
entities.extend(
LaMarzoccoStatisticSensorEntity(coordinator, description)
LaMarzoccoStatisticSensorEntity(statistic_coordinators, description)
for description in STATISTIC_ENTITIES
if description.supported_fn(coordinator)
if description.supported_fn(statistic_coordinators)
)
async_add_entities(entities)
@@ -93,7 +93,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
DOMAIN,
SERVICE_VOLUME_SET,
required_domains={DOMAIN},
required_states={MediaPlayerState.PLAYING},
required_features=MediaPlayerEntityFeature.VOLUME_SET,
required_slots={
ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo(
@@ -159,7 +158,6 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler):
DOMAIN,
SERVICE_MEDIA_PLAY,
required_domains={DOMAIN},
required_states={MediaPlayerState.PAUSED},
description="Resumes a media player",
platforms={DOMAIN},
device_classes={MediaPlayerDeviceClass},
+2 -2
View File
@@ -57,8 +57,8 @@ ATA_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATA_HVAC_MODE_LOOKUP.items()}
ATW_ZONE_HVAC_MODE_LOOKUP = {
atw.ZONE_OPERATION_MODE_HEAT: HVACMode.HEAT,
atw.ZONE_OPERATION_MODE_COOL: HVACMode.COOL,
atw.ZONE_STATUS_HEAT: HVACMode.HEAT,
atw.ZONE_STATUS_COOL: HVACMode.COOL,
}
ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP.items()}
+14 -6
View File
@@ -498,8 +498,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
):
errors[CONF_MAX_KELVIN] = "max_below_min_kelvin"
errors[CONF_MIN_KELVIN] = "max_below_min_kelvin"
errors["advanced_settings"] = "max_below_min_kelvin"
return errors
@@ -1276,7 +1275,10 @@ def validate_user_input(
try:
validator(value)
except (ValueError, vol.Error, vol.Invalid):
errors[field] = data_schema_fields[field].error or "invalid_input"
data_schema_field = data_schema_fields[field]
errors[data_schema_field.section or field] = (
data_schema_field.error or "invalid_input"
)
if config_validator is not None:
if TYPE_CHECKING:
@@ -1385,8 +1387,11 @@ def subentry_schema_default_data_from_fields(
return {
key: field.default
for key, field in data_schema_fields.items()
if field.is_schema_default
or (field.default is not vol.UNDEFINED and key not in component_data)
if _check_conditions(field, component_data)
and (
field.is_schema_default
or (field.default is not vol.UNDEFINED and key not in component_data)
)
}
@@ -2212,7 +2217,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
for component_data in self._subentry_data["components"].values():
platform = component_data[CONF_PLATFORM]
subentry_default_data = subentry_schema_default_data_from_fields(
PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data
COMMON_ENTITY_FIELDS
| PLATFORM_ENTITY_FIELDS[platform]
| PLATFORM_MQTT_FIELDS[platform],
component_data,
)
component_data.update(subentry_default_data)
@@ -6,7 +6,6 @@ import logging
from typing import Any
from pypoint import PointSession
from tempora.utc import fromtimestamp
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -62,7 +61,9 @@ class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
or device.device_id not in self.device_updates
or self.device_updates[device.device_id] < last_updated
):
self.device_updates[device.device_id] = last_updated or fromtimestamp(0)
self.device_updates[device.device_id] = (
last_updated or datetime.fromtimestamp(0)
)
self.data[device.device_id] = {
k: await device.sensor(k)
for k in ("temperature", "humidity", "sound_pressure")
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.3.0"]
"requirements": ["renault-api==0.3.1"]
}
+9 -1
View File
@@ -23,7 +23,7 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -380,6 +380,14 @@ def migrate_entity_ids(
if ch is None or is_chime:
continue # Do not consider the NVR itself or chimes
# Check for wrongfully added MAC of the NVR/Hub to the camera
# Can be removed in HA 2025.12
host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address)
if host_connnection in device.connections:
new_connections = device.connections.copy()
new_connections.remove(host_connnection)
device_reg.async_update_device(device.id, new_connections=new_connections)
ch_device_ids[device.id] = ch
if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch):
if host.api.supported(None, "UID"):
@@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.0"]
"requirements": ["pysmartthings==3.2.1"]
}
@@ -26,6 +26,7 @@ class SmartThingsSelectDescription(SelectEntityDescription):
options_attribute: Attribute
status_attribute: Attribute
command: Command
default_options: list[str] | None = None
CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
@@ -46,6 +47,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
options_attribute=Attribute.SUPPORTED_MACHINE_STATES,
status_attribute=Attribute.MACHINE_STATE,
command=Command.SET_MACHINE_STATE,
default_options=["run", "pause", "stop"],
),
Capability.WASHER_OPERATING_STATE: SmartThingsSelectDescription(
key=Capability.WASHER_OPERATING_STATE,
@@ -55,6 +57,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
options_attribute=Attribute.SUPPORTED_MACHINE_STATES,
status_attribute=Attribute.MACHINE_STATE,
command=Command.SET_MACHINE_STATE,
default_options=["run", "pause", "stop"],
),
Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT,
@@ -114,8 +117,12 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity):
@property
def options(self) -> list[str]:
"""Return the list of options."""
return self.get_attribute_value(
self.entity_description.key, self.entity_description.options_attribute
return (
self.get_attribute_value(
self.entity_description.key, self.entity_description.options_attribute
)
or self.entity_description.default_options
or []
)
@property
@@ -631,7 +631,7 @@ CAPABILITY_TO_SENSORS: dict[
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
translation_key="power_energy",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
@@ -290,8 +290,10 @@
"options": {
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
"battery": "[%key:component::sensor::entity_component::battery::name%]",
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
@@ -302,6 +304,7 @@
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
@@ -338,6 +341,7 @@
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
},
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.5.5", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.6.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
+1
View File
@@ -97,6 +97,7 @@ SKU_TO_BASE_DEVICE = {
"LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S
"LAP-V102S-WEU": "Vital100S", # Alt ID Model Vital100S
"LAP-V102S-WUK": "Vital100S", # Alt ID Model Vital100S
"LAP-V102S-AUSR": "Vital100S", # Alt ID Model Vital100S
"EverestAir": "EverestAir",
"LAP-EL551S-AUS": "EverestAir", # Alt ID Model EverestAir
"LAP-EL551S-AEUR": "EverestAir", # Alt ID Model EverestAir
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/weheat",
"iot_class": "cloud_polling",
"requirements": ["weheat==2025.3.7"]
"requirements": ["weheat==2025.4.29"]
}
+36 -1
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from contextlib import suppress
import dataclasses
from functools import partial, wraps
from typing import Any, Concatenate, Literal, cast
@@ -86,6 +88,7 @@ from .const import (
DATA_CLIENT,
DOMAIN,
EVENT_DEVICE_ADDED_TO_REGISTRY,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
USER_AGENT,
)
from .helpers import (
@@ -182,6 +185,8 @@ STRATEGY = "strategy"
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41
MINIMUM_QR_STRING_LENGTH = 52
HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60
# Helper schemas
PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All(
@@ -2816,6 +2821,7 @@ async def websocket_hard_reset_controller(
driver: Driver,
) -> None:
"""Hard reset controller."""
unsubs: list[Callable[[], None]]
@callback
def async_cleanup() -> None:
@@ -2831,13 +2837,28 @@ async def websocket_hard_reset_controller(
connection.send_result(msg[ID], device.id)
async_cleanup()
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
wait_driver_ready = asyncio.Event()
msg[DATA_UNSUBSCRIBE] = unsubs = [
async_dispatcher_connect(
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added
)
),
driver.once("driver ready", set_driver_ready),
]
await driver.async_hard_reset()
with suppress(TimeoutError):
async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await hass.config_entries.async_reload(entry.entry_id)
@websocket_api.websocket_command(
{
@@ -3043,14 +3064,28 @@ async def websocket_restore_nvm(
)
)
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
wait_driver_ready = asyncio.Event()
# Set up subscription for progress events
connection.subscriptions[msg["id"]] = async_cleanup
msg[DATA_UNSUBSCRIBE] = unsubs = [
controller.on("nvm convert progress", forward_progress),
controller.on("nvm restore progress", forward_progress),
driver.once("driver ready", set_driver_ready),
]
await controller.async_restore_nvm_base64(msg["data"])
with suppress(TimeoutError):
async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await hass.config_entries.async_reload(entry.entry_id)
connection.send_message(
websocket_api.event_message(
msg[ID],
@@ -67,6 +67,7 @@ from .const import (
CONF_USE_ADDON,
DATA_CLIENT,
DOMAIN,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
)
_LOGGER = logging.getLogger(__name__)
@@ -78,7 +79,6 @@ ADDON_SETUP_TIMEOUT = 5
ADDON_SETUP_TIMEOUT_ROUNDS = 40
CONF_EMULATE_HARDWARE = "emulate_hardware"
CONF_LOG_LEVEL = "log_level"
RESTORE_NVM_DRIVER_READY_TIMEOUT = 60
SERVER_VERSION_TIMEOUT = 10
ADDON_LOG_LEVELS = {
@@ -907,10 +907,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Reset the current controller, and instruct the user to unplug it."""
if user_input is not None:
config_entry = self._reconfigure_config_entry
assert config_entry is not None
# Unload the config entry before stopping the add-on.
await self.hass.config_entries.async_unload(config_entry.entry_id)
if self.usb_path:
# USB discovery was used, so the device is already known.
await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path})
@@ -925,6 +921,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Failed to reset controller: %s", err)
return self.async_abort(reason="reset_failed")
config_entry = self._reconfigure_config_entry
assert config_entry is not None
# Unload the config entry before asking the user to unplug the controller.
await self.hass.config_entries.async_unload(config_entry.entry_id)
return self.async_show_form(
step_id="instruct_unplug",
description_placeholders={
@@ -201,3 +201,7 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE,
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
}
# Other constants
RESTORE_NVM_DRIVER_READY_TIMEOUT = 60
@@ -1204,7 +1204,7 @@ DISCOVERY_SCHEMAS = [
property={RESET_METER_PROPERTY},
type={ValueType.BOOLEAN},
),
entity_category=EntityCategory.DIAGNOSTIC,
entity_category=EntityCategory.CONFIG,
),
ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR,
+1 -1
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0b6"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
+4 -2
View File
@@ -575,9 +575,11 @@ class DeviceRegistryItems[_EntryTypeT: (DeviceEntry, DeletedDeviceEntry)](
"""Unindex an entry."""
old_entry = self.data[key]
for connection in old_entry.connections:
del self._connections[connection]
if connection in self._connections:
del self._connections[connection]
for identifier in old_entry.identifiers:
del self._identifiers[identifier]
if identifier in self._identifiers:
del self._identifiers[identifier]
def get_entry(
self,
+3 -3
View File
@@ -2,7 +2,7 @@
aiodhcpwatcher==1.1.1
aiodiscover==2.6.1
aiodns==3.3.0
aiodns==3.4.0
aiohasupervisor==0.3.1
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.2.3
@@ -38,8 +38,8 @@ habluetooth==3.48.2
hass-nabucasa==0.96.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250502.1
home-assistant-intents==2025.4.30
home-assistant-frontend==20250509.0
home-assistant-intents==2025.5.7
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6
+3 -3
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.5.0b6"
version = "2025.5.1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -23,7 +23,7 @@ classifiers = [
]
requires-python = ">=3.13.2"
dependencies = [
"aiodns==3.3.0",
"aiodns==3.4.0",
# Integrations may depend on hassio integration without listing it to
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11
@@ -66,7 +66,7 @@ dependencies = [
# onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"home-assistant-intents==2025.4.30",
"home-assistant-intents==2025.5.7",
"ifaddr==0.2.0",
"Jinja2==3.1.6",
"lru-dict==1.3.0",
+2 -2
View File
@@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiodns==3.3.0
aiodns==3.4.0
aiohasupervisor==0.3.1
aiohttp==3.11.18
aiohttp_cors==0.7.0
@@ -27,7 +27,7 @@ hass-nabucasa==0.96.0
hassil==2.2.3
httpx==0.28.1
home-assistant-bluetooth==1.13.1
home-assistant-intents==2025.4.30
home-assistant-intents==2025.5.7
ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0
+12 -12
View File
@@ -223,7 +223,7 @@ aiodhcpwatcher==1.1.1
aiodiscover==2.6.1
# homeassistant.components.dnsip
aiodns==3.3.0
aiodns==3.4.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -628,7 +628,7 @@ blockchain==1.4.4
bluecurrent-api==1.2.3
# homeassistant.components.bluemaestro
bluemaestro-ble==0.4.0
bluemaestro-ble==0.4.1
# homeassistant.components.decora
# bluepy==1.3.0
@@ -762,7 +762,7 @@ debugpy==1.8.13
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==13.0.1
deebot-client==13.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -782,7 +782,7 @@ denonavr==1.0.1
devialet==1.5.7
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.3
devolo-home-control-api==0.19.0
# homeassistant.components.devolo_home_network
devolo-plc-api==1.5.1
@@ -958,7 +958,7 @@ fnv-hash-fast==1.5.0
foobot_async==1.0.0
# homeassistant.components.forecast_solar
forecast-solar==4.1.0
forecast-solar==4.2.0
# homeassistant.components.fortios
fortiosapi==1.0.5
@@ -1161,10 +1161,10 @@ hole==0.8.0
holidays==0.70
# homeassistant.components.frontend
home-assistant-frontend==20250502.1
home-assistant-frontend==20250509.0
# homeassistant.components.conversation
home-assistant-intents==2025.4.30
home-assistant-intents==2025.5.7
# homeassistant.components.homematicip_cloud
homematicip==2.0.1.1
@@ -2093,7 +2093,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lamarzocco
pylamarzocco==2.0.0
pylamarzocco==2.0.1
# homeassistant.components.lastfm
pylast==5.1.0
@@ -2326,7 +2326,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==3.2.0
pysmartthings==3.2.1
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2631,7 +2631,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.3.0
renault-api==0.3.1
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2975,7 +2975,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.5.5
uiprotect==7.6.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7
@@ -3074,7 +3074,7 @@ webio-api==0.1.11
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2025.3.7
weheat==2025.4.29
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.20.0
+12 -12
View File
@@ -211,7 +211,7 @@ aiodhcpwatcher==1.1.1
aiodiscover==2.6.1
# homeassistant.components.dnsip
aiodns==3.3.0
aiodns==3.4.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -556,7 +556,7 @@ blinkpy==0.23.0
bluecurrent-api==1.2.3
# homeassistant.components.bluemaestro
bluemaestro-ble==0.4.0
bluemaestro-ble==0.4.1
# homeassistant.components.bluetooth
bluetooth-adapters==0.21.4
@@ -653,7 +653,7 @@ dbus-fast==2.43.0
debugpy==1.8.13
# homeassistant.components.ecovacs
deebot-client==13.0.1
deebot-client==13.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -673,7 +673,7 @@ denonavr==1.0.1
devialet==1.5.7
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.3
devolo-home-control-api==0.19.0
# homeassistant.components.devolo_home_network
devolo-plc-api==1.5.1
@@ -818,7 +818,7 @@ fnv-hash-fast==1.5.0
foobot_async==1.0.0
# homeassistant.components.forecast_solar
forecast-solar==4.1.0
forecast-solar==4.2.0
# homeassistant.components.freebox
freebox-api==1.2.2
@@ -991,10 +991,10 @@ hole==0.8.0
holidays==0.70
# homeassistant.components.frontend
home-assistant-frontend==20250502.1
home-assistant-frontend==20250509.0
# homeassistant.components.conversation
home-assistant-intents==2025.4.30
home-assistant-intents==2025.5.7
# homeassistant.components.homematicip_cloud
homematicip==2.0.1.1
@@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8
# homeassistant.components.lamarzocco
pylamarzocco==2.0.0
pylamarzocco==2.0.1
# homeassistant.components.lastfm
pylast==5.1.0
@@ -1899,7 +1899,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==3.2.0
pysmartthings==3.2.1
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2138,7 +2138,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.3.0
renault-api==0.3.1
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2404,7 +2404,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.5.5
uiprotect==7.6.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7
@@ -2485,7 +2485,7 @@ webio-api==0.1.11
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2025.3.7
weheat==2025.4.29
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.20.0
+1 -1
View File
@@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.25.1 tqdm==4.67.1 ruff==0.11.0 \
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.4.30 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
+2 -2
View File
@@ -177,7 +177,7 @@ async def _test_downloading_encrypted_backup(
enc_metadata = json.loads(outer_tar.extractfile("./backup.json").read())
assert enc_metadata["protected"] is True
with (
outer_tar.extractfile("core.tar.gz") as inner_tar_file,
outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file,
pytest.raises(tarfile.ReadError, match="file could not be opened"),
):
# pylint: disable-next=consider-using-with
@@ -209,7 +209,7 @@ async def _test_downloading_encrypted_backup(
dec_metadata = json.loads(outer_tar.extractfile("./backup.json").read())
assert dec_metadata == enc_metadata | {"protected": False}
with (
outer_tar.extractfile("core.tar.gz") as inner_tar_file,
outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file,
tarfile.open(fileobj=inner_tar_file, mode="r") as inner_tar,
):
assert inner_tar.getnames() == [
+46 -18
View File
@@ -174,7 +174,10 @@ async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None:
)
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -218,7 +221,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_reader(
"""Test the decrypted backup streamer."""
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -253,7 +259,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_writer(
"""Test the decrypted backup streamer."""
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -283,7 +292,10 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) ->
"""Test the decrypted backup streamer with wrong password."""
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -320,7 +332,10 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None:
)
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -353,15 +368,16 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None:
bytes.fromhex("00000000000000000000000000000000"),
)
encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2")
assert encryptor.backup() == dataclasses.replace(
backup, protected=True, size=backup.size + len(expected_padding)
)
encrypted_stream = await encryptor.open_stream()
encrypted_output = b""
async for chunk in encrypted_stream:
encrypted_output += chunk
await encryptor.wait()
assert encryptor.backup() == dataclasses.replace(
backup, protected=True, size=backup.size + len(expected_padding)
)
encrypted_stream = await encryptor.open_stream()
encrypted_output = b""
async for chunk in encrypted_stream:
encrypted_output += chunk
await encryptor.wait()
# Expect the output to match the stored encrypted backup file, with additional
# padding.
@@ -377,7 +393,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_reader(
"test_backups/c0cb53bd.tar.decrypted", DOMAIN
)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -414,7 +433,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_writer(
"test_backups/c0cb53bd.tar.decrypted", DOMAIN
)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -447,7 +469,10 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No
)
encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -490,7 +515,7 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No
await encryptor1.wait()
await encryptor2.wait()
# Output from the two streames should differ but have the same length.
# Output from the two streams should differ but have the same length.
assert encrypted_output1 != encrypted_output3
assert len(encrypted_output1) == len(encrypted_output3)
@@ -508,7 +533,10 @@ async def test_encrypted_backup_streamer_error(hass: HomeAssistant) -> None:
"test_backups/c0cb53bd.tar.decrypted", DOMAIN
)
backup = AgentBackup(
addons=["addon_1", "addon_2"],
addons=[
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
],
backup_id="1234",
date="2024-12-02T07:23:58.261875-05:00",
database_included=False,
@@ -1,5 +1,6 @@
"""Mocks for tests."""
from datetime import UTC
from typing import Any
from unittest.mock import MagicMock
@@ -28,6 +29,7 @@ class BinarySensorPropertyMock(BinarySensorProperty):
def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called
"""Initialize the mock."""
self._logger = MagicMock()
self._timezone = UTC
self.element_uid = "Test"
self.key_count = 1
self.sensor_type = "door"
@@ -41,6 +43,7 @@ class BinarySwitchPropertyMock(BinarySwitchProperty):
def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called
"""Initialize the mock."""
self._logger = MagicMock()
self._timezone = UTC
self.element_uid = "Test"
self.state = False
@@ -51,6 +54,7 @@ class ConsumptionPropertyMock(ConsumptionProperty):
def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called
"""Initialize the mock."""
self._logger = MagicMock()
self._timezone = UTC
self.element_uid = "devolo.Meter:Test"
self.current_unit = "W"
self.total_unit = "kWh"
@@ -68,6 +72,7 @@ class MultiLevelSensorPropertyMock(MultiLevelSensorProperty):
self._unit = "°C"
self._value = 20
self._logger = MagicMock()
self._timezone = UTC
class BrightnessSensorPropertyMock(MultiLevelSensorProperty):
@@ -80,6 +85,7 @@ class BrightnessSensorPropertyMock(MultiLevelSensorProperty):
self._unit = "%"
self._value = 20
self._logger = MagicMock()
self._timezone = UTC
class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty):
@@ -92,6 +98,7 @@ class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty):
self.max = 24
self._value = 20
self._logger = MagicMock()
self._timezone = UTC
class SirenPropertyMock(MultiLevelSwitchProperty):
@@ -105,6 +112,7 @@ class SirenPropertyMock(MultiLevelSwitchProperty):
self.switch_type = "tone"
self._value = 0
self._logger = MagicMock()
self._timezone = UTC
class SettingsMock(SettingsProperty):
@@ -113,6 +121,7 @@ class SettingsMock(SettingsProperty):
def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called
"""Initialize the mock."""
self._logger = MagicMock()
self._timezone = UTC
self.name = "Test"
self.zone = "Test"
self.tone = 1
+22 -3
View File
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utcnow
from . import FritzDeviceCoverMock, FritzDeviceSwitchMock
from . import FritzDeviceCoverMock, FritzDeviceSwitchMock, FritzEntityBaseMock
from .const import MOCK_CONFIG
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -84,6 +84,8 @@ async def test_coordinator_automatic_registry_cleanup(
entity_registry: er.EntityRegistry,
) -> None:
"""Test automatic registry cleanup."""
# init with 2 devices and 1 template
fritz().get_devices.return_value = [
FritzDeviceSwitchMock(
ain="fake ain switch",
@@ -96,6 +98,13 @@ async def test_coordinator_automatic_registry_cleanup(
name="fake_cover",
),
]
fritz().get_templates.return_value = [
FritzEntityBaseMock(
ain="fake ain template",
device_and_unit_id=("fake ain template", None),
name="fake_template",
)
]
entry = MockConfigEntry(
domain=FB_DOMAIN,
data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
@@ -105,9 +114,10 @@ async def test_coordinator_automatic_registry_cleanup(
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 19
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 20
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 3
# remove one device, keep the template
fritz().get_devices.return_value = [
FritzDeviceSwitchMock(
ain="fake ain switch",
@@ -119,5 +129,14 @@ async def test_coordinator_automatic_registry_cleanup(
async_fire_time_changed(hass, utcnow() + timedelta(seconds=35))
await hass.async_block_till_done(wait_background_tasks=True)
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 13
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2
# remove the template, keep the device
fritz().get_templates.return_value = []
async_fire_time_changed(hass, utcnow() + timedelta(seconds=35))
await hass.async_block_till_done(wait_background_tasks=True)
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1
@@ -34,9 +34,11 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
UnitOfTemperature,
)
from homeassistant.core import Event, HomeAssistant
@@ -437,6 +439,22 @@ async def test_expose_linked_sensors(
assert acc.char_air_quality.value == 1
assert len(broker.mock_calls) == 0
# Updated temperature with different unit should reflect in HomeKit
broker = MagicMock()
acc.char_current_temperature.broker = broker
hass.states.async_set(
temperature_entity_id,
60,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT,
},
)
await hass.async_block_till_done()
assert acc.char_current_temperature.value == 15.6
assert len(broker.mock_calls) == 2
broker.reset_mock()
# Updated temperature should reflect in HomeKit
broker = MagicMock()
acc.char_current_temperature.broker = broker
@@ -21,37 +21,47 @@ from .const import TEST_MOWER_ID
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_lawn_mower_states(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
values: dict[str, MowerAttributes],
) -> None:
"""Test lawn_mower state."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("lawn_mower.test_mower_1")
assert state is not None
assert state.state == LawnMowerActivity.DOCKED
for activity, state, expected_state in (
@pytest.mark.parametrize(
("activity", "mower_state", "expected_state"),
[
(MowerActivities.UNKNOWN, MowerStates.PAUSED, LawnMowerActivity.PAUSED),
(MowerActivities.MOWING, MowerStates.NOT_APPLICABLE, LawnMowerActivity.MOWING),
(MowerActivities.MOWING, MowerStates.IN_OPERATION, LawnMowerActivity.MOWING),
(MowerActivities.NOT_APPLICABLE, MowerStates.ERROR, LawnMowerActivity.ERROR),
(
MowerActivities.GOING_HOME,
MowerStates.IN_OPERATION,
LawnMowerActivity.RETURNING,
),
):
values[TEST_MOWER_ID].mower.activity = activity
values[TEST_MOWER_ID].mower.state = state
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("lawn_mower.test_mower_1")
assert state.state == expected_state
(
MowerActivities.NOT_APPLICABLE,
MowerStates.IN_OPERATION,
LawnMowerActivity.MOWING,
),
],
)
async def test_lawn_mower_states(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
values: dict[str, MowerAttributes],
activity: MowerActivities,
mower_state: MowerStates,
expected_state: LawnMowerActivity,
) -> None:
"""Test lawn_mower state."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("lawn_mower.test_mower_1")
assert state is not None
assert state.state == LawnMowerActivity.DOCKED
values[TEST_MOWER_ID].mower.activity = activity
values[TEST_MOWER_ID].mower.state = mower_state
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("lawn_mower.test_mower_1")
assert state.state == expected_state
@pytest.mark.parametrize(
@@ -104,19 +104,6 @@ async def test_unpause_media_player_intent(hass: HomeAssistant) -> None:
assert call.service == SERVICE_MEDIA_PLAY
assert call.data == {"entity_id": entity_id}
# Test if not paused
hass.states.async_set(
entity_id,
STATE_PLAYING,
)
with pytest.raises(intent.MatchFailedError):
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_MEDIA_UNPAUSE,
)
async def test_next_media_player_intent(hass: HomeAssistant) -> None:
"""Test HassMediaNext intent for media players."""
@@ -245,17 +232,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None:
assert call.service == SERVICE_VOLUME_SET
assert call.data == {"entity_id": entity_id, "volume_level": 0.5}
# Test if not playing
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
with pytest.raises(intent.MatchFailedError):
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_SET_VOLUME,
{"volume_level": {"value": 50}},
)
# Test feature not supported
hass.states.async_set(
entity_id,
+4
View File
@@ -153,6 +153,10 @@ MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = {
"state_topic": "test-topic",
"color_temp_kelvin": True,
"state_value_template": "{{ value_json.value }}",
"brightness_scale": 255,
"max_kelvin": 6535,
"min_kelvin": 2000,
"white_scale": 255,
"entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2",
},
}
+10 -2
View File
@@ -2817,14 +2817,22 @@ async def test_migrate_of_incompatible_config_entry(
},
{"state_topic": "invalid_subscribe_topic"},
),
(
{
"command_topic": "test-topic",
"light_brightness_settings": {
"brightness_command_topic": "test-topic#invalid"
},
},
{"light_brightness_settings": "invalid_publish_topic"},
),
(
{
"command_topic": "test-topic",
"advanced_settings": {"max_kelvin": 2000, "min_kelvin": 2000},
},
{
"max_kelvin": "max_below_min_kelvin",
"min_kelvin": "max_below_min_kelvin",
"advanced_settings": "max_below_min_kelvin",
},
),
),
@@ -1005,102 +1005,6 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_driver_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Driver door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'driver_door_status',
'unique_id': 'vf1twingoiiivin_driver_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-TWINGO-III Driver door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_driver_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_hatch',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Hatch',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'hatch_status',
'unique_id': 'vf1twingoiiivin_hatch_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-TWINGO-III Hatch',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_hatch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hvac-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1148,102 +1052,6 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.LOCK: 'lock'>,
'original_icon': None,
'original_name': 'Lock',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'vf1twingoiiivin_lock_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'lock',
'friendly_name': 'REG-TWINGO-III Lock',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Passenger door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'passenger_door_status',
'unique_id': 'vf1twingoiiivin_passenger_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-TWINGO-III Passenger door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_plug-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1292,102 +1100,6 @@
'state': 'on',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Rear left door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'rear_left_door_status',
'unique_id': 'vf1twingoiiivin_rear_left_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-TWINGO-III Rear left door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Rear right door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'rear_right_door_status',
'unique_id': 'vf1twingoiiivin_rear_right_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-TWINGO-III Rear right door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_binary_sensors[zoe_40][binary_sensor.reg_zoe_40_charging-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1579,102 +1291,6 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_driver_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Driver door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'driver_door_status',
'unique_id': 'vf1zoe50vin_driver_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-ZOE-50 Driver door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_driver_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_hatch',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Hatch',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'hatch_status',
'unique_id': 'vf1zoe50vin_hatch_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-ZOE-50 Hatch',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_hatch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hvac-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1722,102 +1338,6 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.LOCK: 'lock'>,
'original_icon': None,
'original_name': 'Lock',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'vf1zoe50vin_lock_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'lock',
'friendly_name': 'REG-ZOE-50 Lock',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_passenger_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Passenger door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'passenger_door_status',
'unique_id': 'vf1zoe50vin_passenger_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-ZOE-50 Passenger door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_passenger_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_plug-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1866,99 +1386,3 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Rear left door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'rear_left_door_status',
'unique_id': 'vf1zoe50vin_rear_left_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-ZOE-50 Rear left door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Rear right door',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'rear_right_door_status',
'unique_id': 'vf1zoe50vin_rear_right_door_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'REG-ZOE-50 Rear right door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
@@ -3211,100 +3211,6 @@
'state': 'unknown',
})
# ---
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-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.reg_twingo_iii_remote_engine_start',
'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': 'Remote engine start',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'res_state',
'unique_id': 'vf1twingoiiivin_res_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-TWINGO-III Remote engine start',
}),
'context': <ANY>,
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-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.reg_twingo_iii_remote_engine_start_code',
'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': 'Remote engine start code',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'res_state_code',
'unique_id': 'vf1twingoiiivin_res_state_code',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-TWINGO-III Remote engine start code',
}),
'context': <ANY>,
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[zoe_40][sensor.reg_zoe_40_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -4737,97 +4643,3 @@
'state': 'unplugged',
})
# ---
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-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.reg_zoe_50_remote_engine_start',
'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': 'Remote engine start',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'res_state',
'unique_id': 'vf1zoe50vin_res_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-ZOE-50 Remote engine start',
}),
'context': <ANY>,
'entity_id': 'sensor.reg_zoe_50_remote_engine_start',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Stopped, ready for RES',
})
# ---
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-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.reg_zoe_50_remote_engine_start_code',
'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': 'Remote engine start code',
'platform': 'renault',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'res_state_code',
'unique_id': 'vf1zoe50vin_res_state_code',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-ZOE-50 Remote engine start code',
}),
'context': <ANY>,
'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
+2 -2
View File
@@ -197,7 +197,7 @@ async def test_sensor_throttling_after_init(
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_50", 1, 420), # 7 coordinators => 7 minutes interval
("zoe_50", 1, 300), # 5 coordinators => 5 minutes interval
("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval
("multi", 2, 480), # 8 coordinators => 8 minutes interval
],
@@ -236,7 +236,7 @@ async def test_dynamic_scan_interval(
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_50", 1, 300), # (7-2) coordinators => 5 minutes interval
("zoe_50", 1, 240), # (6-2) coordinators => 4 minutes interval
("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval
("multi", 2, 360), # (8-2) coordinators => 6 minutes interval
],
+51 -1
View File
@@ -39,7 +39,7 @@ from homeassistant.helpers import (
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.setup import async_setup_component
from .conftest import (
@@ -51,6 +51,7 @@ from .conftest import (
TEST_HOST,
TEST_HOST_MODEL,
TEST_MAC,
TEST_MAC_CAM,
TEST_NVR_NAME,
TEST_PORT,
TEST_PRIVACY,
@@ -614,6 +615,55 @@ async def test_migrate_with_already_existing_entity(
assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id)
async def test_cleanup_mac_connection(
hass: HomeAssistant,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test cleanup of the MAC of a IPC which was set to the MAC of the host."""
reolink_connect.channels = [0]
reolink_connect.baichuan.mac_address.return_value = None
entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio"
dev_id = f"{TEST_UID}_{TEST_UID_CAM}"
domain = Platform.SWITCH
dev_entry = device_registry.async_get_or_create(
identifiers={(DOMAIN, dev_id)},
connections={(CONNECTION_NETWORK_MAC, TEST_MAC)},
config_entry_id=config_entry.entry_id,
disabled_by=None,
)
entity_registry.async_get_or_create(
domain=domain,
platform=DOMAIN,
unique_id=entity_id,
config_entry=config_entry,
suggested_object_id=entity_id,
disabled_by=None,
device_id=dev_entry.id,
)
assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id)
device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)})
assert device
assert device.connections == {(CONNECTION_NETWORK_MAC, TEST_MAC)}
# setup CH 0 and host entities/device
with patch("homeassistant.components.reolink.PLATFORMS", [domain]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id)
device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)})
assert device
assert device.connections == set()
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
async def test_no_repair_issue(
hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry
) -> None:
+1
View File
@@ -122,6 +122,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"da_wm_wd_000001",
"da_wm_wd_000001_1",
"da_wm_wm_01011",
"da_wm_wm_100001",
"da_wm_wm_000001",
"da_wm_wm_000001_1",
"da_wm_sc_000001",
@@ -0,0 +1,154 @@
{
"components": {
"main": {
"ocf": {
"st": {
"value": null,
"timestamp": "2020-10-06T23:01:03.011Z"
},
"mndt": {
"value": null,
"timestamp": "2021-01-28T11:54:37.203Z"
},
"mnfv": {
"value": null,
"timestamp": "2020-12-20T14:21:43.766Z"
},
"mnhw": {
"value": null,
"timestamp": "2021-01-25T22:57:01.985Z"
},
"di": {
"value": "C0972771-01D0-0000-0000-000000000000",
"timestamp": "2019-08-10T18:37:20.487Z"
},
"mnsl": {
"value": null,
"timestamp": "2020-12-20T14:21:31.219Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2019-08-10T18:37:20.514Z"
},
"n": {
"value": "Washer",
"timestamp": "2019-08-10T18:37:20.555Z"
},
"mnmo": {
"value": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000",
"timestamp": "2019-08-10T18:37:20.409Z"
},
"vid": {
"value": "DA-WM-WM-100001",
"timestamp": "2019-08-10T18:37:20.381Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2019-08-10T18:37:20.436Z"
},
"mnml": {
"value": null,
"timestamp": "2021-01-28T11:54:37.092Z"
},
"mnpv": {
"value": null,
"timestamp": "2021-01-26T20:55:28.663Z"
},
"mnos": {
"value": null,
"timestamp": "2021-01-26T20:55:28.411Z"
},
"pi": {
"value": "shp",
"timestamp": "2019-08-10T18:37:20.457Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2019-08-10T18:37:20.534Z"
}
},
"remoteControlStatus": {
"remoteControlEnabled": {
"value": "false",
"timestamp": "2025-04-06T17:30:05.372Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 22100103,
"timestamp": "2022-11-01T11:53:01.255Z"
}
},
"refresh": {},
"samsungce.washerOperatingState": {
"washerJobState": {
"value": "none",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"operatingState": {
"value": "ready",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"supportedOperatingStates": {
"value": ["ready", "running", "paused"],
"timestamp": "2022-11-01T11:53:01.255Z"
},
"scheduledJobs": {
"value": null
},
"scheduledPhases": {
"value": null
},
"progress": {
"value": null
},
"remainingTimeStr": {
"value": "00:57",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"washerJobPhase": {
"value": null
},
"operationTime": {
"value": null
},
"remainingTime": {
"value": 57,
"unit": "min",
"timestamp": "2025-04-18T13:17:00.432Z"
}
},
"execute": {
"data": {
"value": null,
"data": {},
"timestamp": "2020-10-05T02:10:50.602Z"
}
},
"washerOperatingState": {
"completionTime": {
"value": "2025-04-18T14:14:00Z",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"machineState": {
"value": "stop",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"washerJobState": {
"value": "none",
"timestamp": "2025-04-18T13:17:00.432Z"
},
"supportedMachineStates": {
"value": null,
"timestamp": "2020-08-14T14:25:00.803Z"
}
},
"switch": {
"switch": {
"value": null,
"timestamp": "2020-09-13T18:32:28.637Z"
}
}
}
}
}
@@ -0,0 +1,84 @@
{
"items": [
{
"deviceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"name": "Washer",
"label": "Washer",
"manufacturerName": "Samsung Electronics",
"presentationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"ownerId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"deviceTypeName": "Samsung OCF Washer",
"components": [
{
"id": "main",
"label": "Washer",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "remoteControlStatus",
"version": 1
},
{
"id": "washerOperatingState",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.washerOperatingState",
"version": 1
}
],
"categories": [
{
"name": "Washer",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2019-08-10T18:37:20Z",
"profile": {
"id": "REDACTED"
},
"ocf": {
"ocfDeviceType": "oic.d.washer",
"name": "Washer",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000",
"vendorId": "DA-WM-WM-100001",
"lastSignupTime": "2021-01-16T06:29:39.379382Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}
@@ -2089,6 +2089,101 @@
'state': 'on',
})
# ---
# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.washer_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_switch_switch_switch',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Washer Power',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.washer_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.washer_remote_control',
'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': 'Remote control',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'remote_control',
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Washer Remote control',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.washer_remote_control',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[ecobee_sensor][binary_sensor.child_bedroom_motion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -992,6 +992,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_wm_100001]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'TP6X_WA54M8750AV',
'model_id': None,
'name': 'Washer',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[ecobee_sensor]
DeviceRegistryEntrySnapshot({
'area_id': None,
@@ -525,3 +525,61 @@
'state': 'standard',
})
# ---
# name: test_all_entities[da_wm_wm_100001][select.washer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'run',
'pause',
'stop',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.washer',
'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': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'operating_state',
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][select.washer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Washer',
'options': list([
'run',
'pause',
'stop',
]),
}),
'context': <ANY>,
'entity_id': 'select.washer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stop',
})
# ---
@@ -1364,7 +1364,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1402,7 +1402,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'AC Office Granit Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1793,7 +1793,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1831,7 +1831,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Office AirFree Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -2222,7 +2222,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -2260,7 +2260,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Aire Dormitorio Principal Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -4000,7 +4000,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -4038,7 +4038,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -4277,7 +4277,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -4315,7 +4315,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -4554,7 +4554,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -4592,7 +4592,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Frigo Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -5128,7 +5128,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -5166,7 +5166,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Eco Heating System Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -5637,7 +5637,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -5675,7 +5675,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Dishwasher Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -6104,7 +6104,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -6142,7 +6142,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'AirDresser Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -6571,7 +6571,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -6609,7 +6609,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Dryer Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -7038,7 +7038,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -7076,7 +7076,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Seca-Roupa Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -7507,7 +7507,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -7545,7 +7545,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Washer Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -7976,7 +7976,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -8014,7 +8014,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Washing Machine Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -8445,7 +8445,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -8483,7 +8483,7 @@
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Machine à Laver Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -8546,6 +8546,198 @@
'state': '1642.2',
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_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': None,
'entity_id': 'sensor.washer_completion_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': 'Completion time',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'completion_time',
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_completionTime_completionTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Washer Completion time',
}),
'context': <ANY>,
'entity_id': 'sensor.washer_completion_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-04-18T14:14:00+00:00',
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'air_wash',
'ai_rinse',
'ai_spin',
'ai_wash',
'cooling',
'delay_wash',
'drying',
'finish',
'none',
'pre_wash',
'rinse',
'spin',
'wash',
'weight_sensing',
'wrinkle_prevent',
'freeze_protection',
]),
}),
'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.washer_job_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Job state',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'washer_job_state',
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_washerJobState_washerJobState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Washer Job state',
'options': list([
'air_wash',
'ai_rinse',
'ai_spin',
'ai_wash',
'cooling',
'delay_wash',
'drying',
'finish',
'none',
'pre_wash',
'rinse',
'spin',
'wash',
'weight_sensing',
'wrinkle_prevent',
'freeze_protection',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.washer_job_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'none',
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'pause',
'run',
'stop',
]),
}),
'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.washer_machine_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Machine state',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'washer_machine_state',
'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Washer Machine state',
'options': list([
'pause',
'run',
'stop',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.washer_machine_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stop',
})
# ---
# name: test_all_entities[ecobee_sensor][sensor.child_bedroom_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
+177 -69
View File
@@ -5,7 +5,7 @@ from http import HTTPStatus
from io import BytesIO
import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch
import pytest
from zwave_js_server.const import (
@@ -5078,53 +5078,97 @@ async def test_subscribe_node_statistics(
assert msg["error"]["code"] == ERR_NOT_LOADED
@pytest.mark.skip(
reason="The test needs to be updated to reflect what happens when resetting the controller"
)
async def test_hard_reset_controller(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
client,
integration,
listen_block,
client: MagicMock,
integration: MockConfigEntry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test that the hard_reset_controller WS API call works."""
entry = integration
ws_client = await hass_ws_client(hass)
device = device_registry.async_get_device(
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
)
async def async_send_command_driver_ready(
message: dict[str, Any],
require_schema: int | None = None,
) -> dict:
"""Send a command and get a response."""
client.driver.emit(
"driver ready", {"event": "driver ready", "source": "driver"}
)
return {}
client.async_send_command.return_value = {}
await ws_client.send_json(
client.async_send_command.side_effect = async_send_command_driver_ready
await ws_client.send_json_auto_id(
{
ID: 1,
TYPE: "zwave_js/hard_reset_controller",
ENTRY_ID: entry.entry_id,
}
)
listen_block.set()
listen_block.clear()
await hass.async_block_till_done()
msg = await ws_client.receive_json()
device = device_registry.async_get_device(
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
)
assert device is not None
assert msg["result"] == device.id
assert msg["success"]
assert len(client.async_send_command.call_args_list) == 1
assert client.async_send_command.call_args[0][0] == {"command": "driver.hard_reset"}
assert client.async_send_command.call_count == 3
# The first call is the relevant hard reset command.
# 25 is the require_schema parameter.
assert client.async_send_command.call_args_list[0] == call(
{"command": "driver.hard_reset"}, 25
)
client.async_send_command.reset_mock()
# Test sending command with driver not ready and timeout.
async def async_send_command_no_driver_ready(
message: dict[str, Any],
require_schema: int | None = None,
) -> dict:
"""Send a command and get a response."""
return {}
client.async_send_command.side_effect = async_send_command_no_driver_ready
with patch(
"homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT",
new=0,
):
await ws_client.send_json_auto_id(
{
TYPE: "zwave_js/hard_reset_controller",
ENTRY_ID: entry.entry_id,
}
)
msg = await ws_client.receive_json()
device = device_registry.async_get_device(
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
)
assert device is not None
assert msg["result"] == device.id
assert msg["success"]
assert client.async_send_command.call_count == 3
# The first call is the relevant hard reset command.
# 25 is the require_schema parameter.
assert client.async_send_command.call_args_list[0] == call(
{"command": "driver.hard_reset"}, 25
)
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.driver.Driver.async_hard_reset",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
await ws_client.send_json_auto_id(
{
ID: 2,
TYPE: "zwave_js/hard_reset_controller",
ENTRY_ID: entry.entry_id,
}
@@ -5139,9 +5183,8 @@ async def test_hard_reset_controller(
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
await ws_client.send_json(
await ws_client.send_json_auto_id(
{
ID: 3,
TYPE: "zwave_js/hard_reset_controller",
ENTRY_ID: entry.entry_id,
}
@@ -5151,9 +5194,8 @@ async def test_hard_reset_controller(
assert not msg["success"]
assert msg["error"]["code"] == ERR_NOT_LOADED
await ws_client.send_json(
await ws_client.send_json_auto_id(
{
ID: 4,
TYPE: "zwave_js/hard_reset_controller",
ENTRY_ID: "INVALID",
}
@@ -5476,10 +5518,98 @@ async def test_restore_nvm(
# Set up mocks for the controller events
controller = client.driver.controller
# Test restore success
with patch.object(
controller, "async_restore_nvm_base64", return_value=None
) as mock_restore:
async def async_send_command_driver_ready(
message: dict[str, Any],
require_schema: int | None = None,
) -> dict:
"""Send a command and get a response."""
client.driver.emit(
"driver ready", {"event": "driver ready", "source": "driver"}
)
return {}
client.async_send_command.side_effect = async_send_command_driver_ready
# Send the subscription request
await ws_client.send_json_auto_id(
{
"type": "zwave_js/restore_nvm",
"entry_id": integration.entry_id,
"data": "dGVzdA==", # base64 encoded "test"
}
)
# Verify the finished event first
msg = await ws_client.receive_json()
assert msg["type"] == "event"
assert msg["event"]["event"] == "finished"
# Verify subscription success
msg = await ws_client.receive_json()
assert msg["type"] == "result"
assert msg["success"] is True
# Simulate progress events
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 25,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 25
assert msg["event"]["total"] == 100
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 50,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 50
assert msg["event"]["total"] == 100
await hass.async_block_till_done()
# Verify the restore was called
# The first call is the relevant one for nvm restore.
assert client.async_send_command.call_count == 3
assert client.async_send_command.call_args_list[0] == call(
{
"command": "controller.restore_nvm",
"nvmData": "dGVzdA==",
},
require_schema=14,
)
client.async_send_command.reset_mock()
# Test sending command with driver not ready and timeout.
async def async_send_command_no_driver_ready(
message: dict[str, Any],
require_schema: int | None = None,
) -> dict:
"""Send a command and get a response."""
return {}
client.async_send_command.side_effect = async_send_command_no_driver_ready
with patch(
"homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT",
new=0,
):
# Send the subscription request
await ws_client.send_json_auto_id(
{
@@ -5491,6 +5621,7 @@ async def test_restore_nvm(
# Verify the finished event first
msg = await ws_client.receive_json()
assert msg["type"] == "event"
assert msg["event"]["event"] == "finished"
@@ -5499,48 +5630,25 @@ async def test_restore_nvm(
assert msg["type"] == "result"
assert msg["success"] is True
# Simulate progress events
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 25,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 25
assert msg["event"]["total"] == 100
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 50,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 50
assert msg["event"]["total"] == 100
# Wait for the restore to complete
await hass.async_block_till_done()
# Verify the restore was called
assert mock_restore.called
# Verify the restore was called
# The first call is the relevant one for nvm restore.
assert client.async_send_command.call_count == 3
assert client.async_send_command.call_args_list[0] == call(
{
"command": "controller.restore_nvm",
"nvmData": "dGVzdA==",
},
require_schema=14,
)
client.async_send_command.reset_mock()
# Test restore failure
with patch.object(
controller,
"async_restore_nvm_base64",
side_effect=FailedCommand("failed_command", "Restore failed"),
with patch(
f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
# Send the subscription request
await ws_client.send_json_auto_id(
@@ -5554,7 +5662,7 @@ async def test_restore_nvm(
# Verify error response
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "Restore failed"
assert msg["error"]["code"] == "zwave_error"
# Test entry_id not found
await ws_client.send_json_auto_id(
@@ -1109,10 +1109,10 @@ async def test_usb_discovery_migration_driver_ready_timeout(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
assert set_addon_options.call_args == call(
@@ -3776,6 +3776,7 @@ async def test_reconfigure_migrate_with_addon(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
@@ -3790,7 +3791,6 @@ async def test_reconfigure_migrate_with_addon(
},
)
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
assert set_addon_options.call_args == call(
@@ -3918,6 +3918,7 @@ async def test_reconfigure_migrate_driver_ready_timeout(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
@@ -3932,7 +3933,6 @@ async def test_reconfigure_migrate_driver_ready_timeout(
},
)
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
assert result["type"] == FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
assert set_addon_options.call_args == call(
@@ -4108,6 +4108,7 @@ async def test_reconfigure_migrate_start_addon_failure(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
@@ -4202,6 +4203,7 @@ async def test_reconfigure_migrate_restore_failure(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
@@ -4367,6 +4369,7 @@ async def test_choose_serial_port_usb_ports_failure(
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
with patch(
"homeassistant.components.zwave_js.config_flow.async_get_usb_ports",
+8 -1
View File
@@ -431,10 +431,11 @@ async def test_rediscovery(
async def test_aeotec_smart_switch_7(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
aeotec_smart_switch_7: Node,
integration: MockConfigEntry,
) -> None:
"""Test that Smart Switch 7 has a light and a switch entity."""
"""Test Smart Switch 7 discovery."""
state = hass.states.get("light.smart_switch_7")
assert state
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
@@ -443,3 +444,9 @@ async def test_aeotec_smart_switch_7(
state = hass.states.get("switch.smart_switch_7")
assert state
state = hass.states.get("button.smart_switch_7_reset_accumulated_values")
assert state
entity_entry = entity_registry.async_get(state.entity_id)
assert entity_entry
assert entity_entry.entity_category is EntityCategory.CONFIG