mirror of
https://github.com/home-assistant/core.git
synced 2026-05-28 11:43:16 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a89beb3d | |||
| 41f783f14d | |||
| 35397b818d | |||
| d42d02f20a | |||
| 99c445f261 | |||
| 567fe85828 | |||
| fd1a5d0c5a | |||
| 632ec39d53 | |||
| 67b9d28953 |
@@ -39,7 +39,7 @@ on:
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.6"
|
||||
HA_SHORT_VERSION: "2026.7"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .agent import BackupAgent, LocalBackupAgent, OnProgressCallback
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .models import AgentBackup, BackupNotFound
|
||||
from .models import AgentBackup, BackupNotFound, InvalidBackupFilename
|
||||
from .util import read_backup, suggested_filename
|
||||
|
||||
|
||||
@@ -54,7 +54,13 @@ class CoreLocalBackupAgent(LocalBackupAgent):
|
||||
try:
|
||||
backup = read_backup(backup_path)
|
||||
backups[backup.backup_id] = (backup, backup_path)
|
||||
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
|
||||
except (
|
||||
OSError,
|
||||
TarError,
|
||||
json.JSONDecodeError,
|
||||
KeyError,
|
||||
InvalidBackupFilename,
|
||||
) as err:
|
||||
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
||||
return backups
|
||||
|
||||
@@ -122,7 +128,14 @@ class CoreLocalBackupAgent(LocalBackupAgent):
|
||||
|
||||
def get_new_backup_path(self, backup: AgentBackup) -> Path:
|
||||
"""Return the local path to a new backup."""
|
||||
return self._backup_dir / suggested_filename(backup)
|
||||
candidate = self._backup_dir / suggested_filename(backup)
|
||||
# suggested_filename does not strip separators; refuse paths that would
|
||||
# land outside the backup directory.
|
||||
if candidate.parent != self._backup_dir:
|
||||
raise InvalidBackupFilename(
|
||||
f"Refusing to write outside {self._backup_dir}: {candidate}"
|
||||
)
|
||||
return candidate
|
||||
|
||||
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
|
||||
"""Delete a backup file."""
|
||||
|
||||
@@ -1978,7 +1978,13 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
|
||||
try:
|
||||
backup = await async_add_executor_job(read_backup, temp_file)
|
||||
except (OSError, tarfile.TarError, json.JSONDecodeError, KeyError) as err:
|
||||
except (
|
||||
OSError,
|
||||
tarfile.TarError,
|
||||
json.JSONDecodeError,
|
||||
KeyError,
|
||||
InvalidBackupFilename,
|
||||
) as err:
|
||||
LOGGER.warning("Unable to parse backup %s: %s", temp_file, err)
|
||||
raise
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import copy
|
||||
from dataclasses import dataclass, replace
|
||||
from io import BytesIO
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path, PurePath, PureWindowsPath
|
||||
from queue import SimpleQueue
|
||||
import tarfile
|
||||
import threading
|
||||
@@ -34,7 +34,7 @@ from homeassistant.util.async_iterator import (
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .const import BUF_SIZE, LOGGER, SECURETAR_CREATE_VERSION
|
||||
from .models import AddonInfo, AgentBackup, Folder
|
||||
from .models import AddonInfo, AgentBackup, Folder, InvalidBackupFilename
|
||||
|
||||
|
||||
class DecryptError(HomeAssistantError):
|
||||
@@ -109,6 +109,13 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
||||
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
||||
|
||||
name = cast(str, data["name"])
|
||||
# The name is used to derive the on-disk filename via suggested_filename;
|
||||
# reject anything that could escape the backup directory.
|
||||
safe_name = PureWindowsPath(name).name
|
||||
if safe_name != name or name in ("", ".", ".."):
|
||||
raise InvalidBackupFilename(f"Invalid backup name: {name!r}")
|
||||
|
||||
return AgentBackup(
|
||||
addons=addons,
|
||||
backup_id=cast(str, data["slug"]),
|
||||
@@ -118,7 +125,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
folders=folders,
|
||||
homeassistant_included=homeassistant_included,
|
||||
homeassistant_version=homeassistant_version,
|
||||
name=cast(str, data["name"]),
|
||||
name=name,
|
||||
protected=cast(bool, data.get("protected", False)),
|
||||
size=backup_path.stat().st_size,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, final
|
||||
|
||||
from propcache.api import cached_property
|
||||
@@ -22,6 +23,7 @@ from homeassistant.core import (
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
async_get_hass_or_none,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
@@ -37,6 +39,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
@@ -52,6 +55,8 @@ from .const import (
|
||||
SourceType,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
|
||||
|
||||
|
||||
@@ -164,11 +169,35 @@ class BaseTrackerEntity(Entity):
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_source_type: SourceType
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
"""Post initialisation processing."""
|
||||
super().__init_subclass__(**kwargs)
|
||||
if "battery_level" in cls.__dict__:
|
||||
if cls.__module__.startswith("homeassistant.components."):
|
||||
# Don't ask users to report issue for built in integrations,
|
||||
# they already have issues opened on them.
|
||||
return
|
||||
report_issue = async_suggest_report_issue(
|
||||
async_get_hass_or_none(), module=cls.__module__
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s is overriding the deprecated battery_level property on "
|
||||
"a subclass of BaseTrackerEntity, this will be unsupported from "
|
||||
"Home Assistant 2027.7, please %s"
|
||||
),
|
||||
cls.__module__,
|
||||
cls.__name__,
|
||||
report_issue,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level of the device.
|
||||
|
||||
Percentage from 0-100.
|
||||
|
||||
The property is deprecated and will be removed in Home Assistant 2027.7.
|
||||
"""
|
||||
return None
|
||||
|
||||
@@ -212,13 +241,38 @@ class TrackerEntity(
|
||||
_attr_in_zones: list[str] | None = None
|
||||
_attr_latitude: float | None = None
|
||||
_attr_location_accuracy: float = 0
|
||||
# _attr_location_name is deprecated and will be removed in Home Assistant 2027.7
|
||||
_attr_location_name: str | None = None
|
||||
_attr_longitude: float | None = None
|
||||
_attr_source_type: SourceType = SourceType.GPS
|
||||
|
||||
__active_zone: State | None = None
|
||||
# If we reported setting deprecated _attr_location_name
|
||||
__deprecated_attr_location_name_reported = False
|
||||
__in_zones: list[str] | None = None
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
"""Post initialisation processing."""
|
||||
super().__init_subclass__(**kwargs)
|
||||
if "location_name" in cls.__dict__:
|
||||
if cls.__module__.startswith("homeassistant.components."):
|
||||
# Don't ask users to report issue for built in integrations,
|
||||
# they already have issues opened on them.
|
||||
return
|
||||
report_issue = async_suggest_report_issue(
|
||||
async_get_hass_or_none(), module=cls.__module__
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s is overriding the deprecated location_name property on "
|
||||
"an instance of TrackerEntity, this will be unsupported from "
|
||||
"Home Assistant 2027.7, please %s"
|
||||
),
|
||||
cls.__module__,
|
||||
cls.__name__,
|
||||
report_issue,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling for entities that have location pushed."""
|
||||
@@ -249,7 +303,32 @@ class TrackerEntity(
|
||||
|
||||
@cached_property
|
||||
def location_name(self) -> str | None:
|
||||
"""Return a location name for the current location of the device."""
|
||||
"""Return a location name for the current location of the device.
|
||||
|
||||
The property is deprecated and will be removed in Home Assistant 2027.7.
|
||||
"""
|
||||
if (location_name := self._attr_location_name) is not None:
|
||||
if (
|
||||
not self.__deprecated_attr_location_name_reported
|
||||
and not self.__class__.__module__.startswith(
|
||||
"homeassistant.components."
|
||||
)
|
||||
):
|
||||
report_issue = async_suggest_report_issue(
|
||||
self.hass, module=self.__class__.__module__
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s is setting the deprecated _attr_location_name attribute "
|
||||
"on an instance of TrackerEntity, this will be unsupported from "
|
||||
"Home Assistant 2027.7, please %s"
|
||||
),
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
report_issue,
|
||||
)
|
||||
self.__deprecated_attr_location_name_reported = True
|
||||
return location_name
|
||||
return self._attr_location_name
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -439,6 +439,19 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
allow_multi=True, # also used for climate entity
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SoilMoistureSensor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.MOISTURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
clusters.SoilMeasurement.Attributes.SoilMoistureMeasuredValue,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
|
||||
@@ -52,6 +52,11 @@ class OMIEPriceSensor(CoordinatorEntity[OMIECoordinator], SensorEntity):
|
||||
self._attr_unique_id = pyomie_series_name
|
||||
self._pyomie_series_name = pyomie_series_name
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update this sensor's state from the coordinator results."""
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["wiim.sdk", "async_upnp_client"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["wiim==0.1.2"],
|
||||
"requirements": ["wiim==0.1.4"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -349,15 +349,12 @@ class WiimMediaPlayerEntity(WiimBaseEntity, MediaPlayerEntity):
|
||||
sdk_status_str,
|
||||
)
|
||||
else:
|
||||
self._device.playing_status = sdk_status
|
||||
if sdk_status == SDKPlayingStatus.STOPPED:
|
||||
LOGGER.debug(
|
||||
"Device %s: TransportState is STOPPED."
|
||||
" Resetting media position and metadata",
|
||||
self.entity_id,
|
||||
)
|
||||
self._device.current_position = 0
|
||||
self._device.current_track_duration = 0
|
||||
self._attr_media_position_updated_at = None
|
||||
self._attr_media_duration = None
|
||||
self._attr_media_position = None
|
||||
|
||||
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 6
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.0.dev0"
|
||||
version = "2026.7.0.dev0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+1
-1
@@ -3357,7 +3357,7 @@ whois==0.9.27
|
||||
wiffi==1.1.2
|
||||
|
||||
# homeassistant.components.wiim
|
||||
wiim==0.1.2
|
||||
wiim==0.1.4
|
||||
|
||||
# homeassistant.components.wirelesstag
|
||||
wirelesstagpy==0.8.1
|
||||
|
||||
@@ -2088,6 +2088,36 @@ async def test_receive_backup_path_traversal(
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"/absolute/path",
|
||||
"../parent",
|
||||
"with/slash",
|
||||
],
|
||||
)
|
||||
async def test_receive_backup_rejects_unsafe_inner_name(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Test receive backup rejects an inner name that would escape the backup dir."""
|
||||
await setup_backup_integration(hass)
|
||||
client = await hass_client()
|
||||
|
||||
backup = replace(TEST_BACKUP_ABC123, name=name)
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.read_backup",
|
||||
return_value=backup,
|
||||
):
|
||||
resp = await client.post(
|
||||
"/api/backup/upload?agent_id=backup.local",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
async def test_receive_backup_busy_manager(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
|
||||
@@ -14,6 +14,7 @@ import pytest
|
||||
import securetar
|
||||
|
||||
from homeassistant.components.backup import DOMAIN, AddonInfo, AgentBackup, Folder
|
||||
from homeassistant.components.backup.models import InvalidBackupFilename
|
||||
from homeassistant.components.backup.util import (
|
||||
DecryptedBackupStreamer,
|
||||
EncryptedBackupStreamer,
|
||||
@@ -158,6 +159,37 @@ def test_read_backup(backup_json_content: bytes, expected_backup: AgentBackup) -
|
||||
assert backup == expected_backup
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"/absolute/path",
|
||||
"../parent",
|
||||
"with/slash",
|
||||
"with\\backslash",
|
||||
"C:\\drive\\path",
|
||||
"",
|
||||
".",
|
||||
"..",
|
||||
],
|
||||
)
|
||||
def test_read_backup_rejects_unsafe_name(name: str) -> None:
|
||||
"""Test that read_backup rejects names that could escape the backup directory."""
|
||||
backup_json_content = (
|
||||
b'{"compressed":true,"date":"2024-12-02T07:23:58.261875-05:00","homeassistant":'
|
||||
b'{"exclude_database":true,"version":"2024.12.0.dev0"},"name":"'
|
||||
+ name.encode().replace(b"\\", b"\\\\")
|
||||
+ b'","protected":true,"slug":"455645fe","type":"partial","version":2}'
|
||||
)
|
||||
mock_path = Mock()
|
||||
mock_path.stat.return_value.st_size = 1234
|
||||
|
||||
with patch("homeassistant.components.backup.util.tarfile.open") as mock_open_tar:
|
||||
tar_ctx = mock_open_tar.return_value.__enter__.return_value
|
||||
tar_ctx.extractfile.return_value.read.return_value = backup_json_content
|
||||
with pytest.raises(InvalidBackupFilename):
|
||||
read_backup(mock_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("backup", "password", "validation_result", "expected_messages"),
|
||||
[
|
||||
|
||||
@@ -1379,6 +1379,101 @@ def test_base_tracker_entity() -> None:
|
||||
entity.state_attributes # noqa: B018
|
||||
|
||||
|
||||
def test_battery_level_override_deprecation_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that overriding battery_level in a subclass logs a warning."""
|
||||
error_message = "is overriding the deprecated battery_level property"
|
||||
|
||||
caplog.clear()
|
||||
|
||||
class _SubclassWithOverride(TrackerEntity):
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
return 50
|
||||
|
||||
assert error_message in caplog.text
|
||||
assert _SubclassWithOverride.__name__ in caplog.text
|
||||
|
||||
# No warning for a subclass that does not override battery_level
|
||||
caplog.clear()
|
||||
|
||||
class _SubclassWithoutOverride(TrackerEntity):
|
||||
pass
|
||||
|
||||
assert error_message not in caplog.text
|
||||
|
||||
|
||||
async def test_attr_location_name_deprecation_warning(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that setting _attr_location_name logs a deprecation warning."""
|
||||
error_message = "is setting the deprecated _attr_location_name attribute"
|
||||
|
||||
class _Subclass(TrackerEntity):
|
||||
pass
|
||||
|
||||
# No warning when _attr_location_name is unset (default None)
|
||||
entity_no_attr = _Subclass()
|
||||
entity_no_attr.hass = hass
|
||||
assert entity_no_attr.location_name is None
|
||||
assert error_message not in caplog.text
|
||||
|
||||
# Warning fires when _attr_location_name has a non-None value
|
||||
entity = _Subclass()
|
||||
entity.hass = hass
|
||||
entity._attr_location_name = "the_zone"
|
||||
caplog.clear()
|
||||
assert entity.location_name == "the_zone"
|
||||
assert error_message in caplog.text
|
||||
|
||||
# Warning does not fire again on subsequent access for the same instance
|
||||
caplog.clear()
|
||||
assert entity.location_name == "the_zone"
|
||||
assert error_message not in caplog.text
|
||||
|
||||
# Warning is suppressed for this instance even after the cached value is
|
||||
# invalidated by a subsequent _attr_location_name assignment.
|
||||
entity._attr_location_name = "another_zone"
|
||||
caplog.clear()
|
||||
assert entity.location_name == "another_zone"
|
||||
assert error_message not in caplog.text
|
||||
|
||||
# A fresh instance warns once again
|
||||
entity_new = _Subclass()
|
||||
entity_new.hass = hass
|
||||
entity_new._attr_location_name = "the_zone"
|
||||
caplog.clear()
|
||||
assert entity_new.location_name == "the_zone"
|
||||
assert error_message in caplog.text
|
||||
|
||||
|
||||
def test_location_name_override_deprecation_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that overriding location_name in a subclass logs a warning."""
|
||||
error_message = "is overriding the deprecated location_name property"
|
||||
|
||||
caplog.clear()
|
||||
|
||||
class _SubclassWithOverride(TrackerEntity):
|
||||
@property
|
||||
def location_name(self) -> str | None:
|
||||
return "custom"
|
||||
|
||||
assert error_message in caplog.text
|
||||
assert _SubclassWithOverride.__name__ in caplog.text
|
||||
|
||||
# No warning for a subclass that does not override location_name
|
||||
caplog.clear()
|
||||
|
||||
class _SubclassWithoutOverride(TrackerEntity):
|
||||
pass
|
||||
|
||||
assert error_message not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mac_address", "unique_id"), [(TEST_MAC_ADDRESS, f"{TEST_MAC_ADDRESS}_yo1")]
|
||||
)
|
||||
|
||||
@@ -78,6 +78,7 @@ FIXTURES = [
|
||||
"mock_pressure_sensor",
|
||||
"mock_pump",
|
||||
"mock_room_airconditioner",
|
||||
"mock_soil_sensor",
|
||||
"mock_solar_inverter",
|
||||
"mock_speaker",
|
||||
"mock_switch_unit",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"node_id": 101,
|
||||
"date_commissioned": "2024-11-27T00:00:00.000000",
|
||||
"last_interview": "2024-11-27T00:00:00.000000",
|
||||
"interview_version": 2,
|
||||
"attributes": {
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 22,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"0/29/1": [
|
||||
4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63,
|
||||
64, 65
|
||||
],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1],
|
||||
"0/29/65532": 0,
|
||||
"0/29/65533": 1,
|
||||
"0/29/65528": [],
|
||||
"0/29/65529": [],
|
||||
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/40/0": 1,
|
||||
"0/40/1": "Nabu Casa",
|
||||
"0/40/2": 65521,
|
||||
"0/40/3": "Mock SoilSensor",
|
||||
"0/40/4": 32768,
|
||||
"0/40/5": "Mock Soil Sensor",
|
||||
"0/40/6": "XX",
|
||||
"0/40/7": 0,
|
||||
"0/40/8": "v1.0",
|
||||
"0/40/9": 1,
|
||||
"0/40/10": "v1.0",
|
||||
"0/40/11": "20241127",
|
||||
"0/40/12": "",
|
||||
"0/40/13": "",
|
||||
"0/40/14": "",
|
||||
"0/40/15": "TEST_SN",
|
||||
"0/40/16": false,
|
||||
"0/40/17": true,
|
||||
"0/40/18": "mock-soil-sensor",
|
||||
"0/40/19": {
|
||||
"0": 3,
|
||||
"1": 3
|
||||
},
|
||||
"0/40/65532": 0,
|
||||
"0/40/65533": 1,
|
||||
"0/40/65528": [],
|
||||
"0/40/65529": [],
|
||||
"0/40/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||
65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"1/3/65529": [0, 64],
|
||||
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 69,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"1/29/1": [3, 29, 57, 1072, 40],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [9, 10],
|
||||
"1/29/65532": null,
|
||||
"1/29/65533": 1,
|
||||
"1/29/65528": [],
|
||||
"1/29/65529": [],
|
||||
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/1072/0": {
|
||||
"0": 17,
|
||||
"1": true,
|
||||
"2": 0,
|
||||
"3": 100,
|
||||
"4": []
|
||||
},
|
||||
"1/1072/1": 50,
|
||||
"1/1072/65532": 0,
|
||||
"1/1072/65533": 1,
|
||||
"1/1072/65528": [],
|
||||
"1/1072/65529": [],
|
||||
"1/1072/65531": [0, 1, 65528, 65529, 65531, 65532, 65533]
|
||||
},
|
||||
"available": true,
|
||||
"attribute_subscriptions": []
|
||||
}
|
||||
@@ -18619,6 +18619,61 @@
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_soil_sensor][sensor.mock_soil_sensor_moisture-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mock_soil_sensor_moisture',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Moisture',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MOISTURE: 'moisture'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Moisture',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000065-MatterNodeDevice-1-SoilMoistureSensor-1072-1',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_soil_sensor][sensor.mock_soil_sensor_moisture-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'moisture',
|
||||
'friendly_name': 'Mock Soil Sensor Moisture',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_soil_sensor_moisture',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '50',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_solar_inverter][sensor.mock_solar_inverter_active_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -85,6 +85,25 @@ async def test_humidity_sensor(
|
||||
assert state.state == "40.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_soil_sensor"])
|
||||
async def test_soil_moisture_sensor(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test soil moisture sensor."""
|
||||
state = hass.states.get("sensor.mock_soil_sensor_moisture")
|
||||
assert state
|
||||
assert state.state == "50"
|
||||
|
||||
set_node_attribute(matter_node, 1, 1072, 1, 75)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_soil_sensor_moisture")
|
||||
assert state
|
||||
assert state.state == "75"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_light_sensor"])
|
||||
async def test_light_sensor(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -10,35 +10,35 @@ import pytest
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import spot_price_fetcher
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@freeze_time("2024-01-15T14:01:00Z")
|
||||
async def test_sensor_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pyomie: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_omie_results_jan15: OMIEResults,
|
||||
) -> None:
|
||||
"""Test sensor platform setup."""
|
||||
mock_pyomie.spot_price.side_effect = spot_price_fetcher(
|
||||
{
|
||||
"2024-01-15": mock_omie_results_jan15,
|
||||
}
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
# Should have 2 sensors (PT and ES)
|
||||
assert len(entities) == 2
|
||||
|
||||
unique_ids = {entity.unique_id for entity in entities}
|
||||
expected_ids = {"pt_spot_price", "es_spot_price"}
|
||||
assert unique_ids == expected_ids
|
||||
assert (pt_state := hass.states.get("sensor.omie_portugal_spot_price"))
|
||||
assert (es_state := hass.states.get("sensor.omie_spain_spot_price"))
|
||||
assert pt_state.state == "351151500.0"
|
||||
assert es_state.state == "34151500.0"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass_lisbon")
|
||||
|
||||
@@ -38,5 +38,6 @@ async def fire_transport_update(
|
||||
"""Trigger the registered AVTransport callback on the mock device."""
|
||||
assert mock_device.av_transport_event_callback is not None
|
||||
mock_device.event_data = {"TransportState": transport_state.value}
|
||||
mock_device.playing_status = transport_state
|
||||
mock_device.av_transport_event_callback(MagicMock(), [])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Reference in New Issue
Block a user