From cb3d52e43a5c33418349ef115ab70f2b96d195c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 09:32:07 -0800 Subject: [PATCH 01/11] Protect state.as_dict from mutation --- homeassistant/components/tuya/diagnostics.py | 2 +- homeassistant/core.py | 22 +++++++++++--------- tests/test_core.py | 13 ++++++++++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index f0e5ed2852fae..67bbad0aceb5f 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -157,7 +157,7 @@ def _async_device_as_dict(hass: HomeAssistant, device: TuyaDevice) -> dict[str, state = hass.states.get(entity_entry.entity_id) state_dict = None if state: - state_dict = state.as_dict() + state_dict = dict(state.as_dict()) # Redact the `entity_picture` attribute as it contains a token. if "entity_picture" in state_dict["attributes"]: diff --git a/homeassistant/core.py b/homeassistant/core.py index 30e98da763731..d9bb6daf5b712 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1054,7 +1054,7 @@ def __init__( self.last_changed = last_changed or self.last_updated self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) - self._as_dict: dict[str, Collection[Any]] | None = None + self._as_dict: MappingProxyType[str, Collection[Any]] | None = None @property def name(self) -> str: @@ -1063,7 +1063,7 @@ def name(self) -> str: "_", " " ) - def as_dict(self) -> dict[str, Collection[Any]]: + def as_dict(self) -> MappingProxyType[str, Collection[Any]]: """Return a dict representation of the State. Async friendly. @@ -1077,14 +1077,16 @@ def as_dict(self) -> dict[str, Collection[Any]]: last_updated_isoformat = last_changed_isoformat else: last_updated_isoformat = self.last_updated.isoformat() - self._as_dict = { - "entity_id": self.entity_id, - "state": self.state, - "attributes": dict(self.attributes), - "last_changed": last_changed_isoformat, - "last_updated": last_updated_isoformat, - "context": self.context.as_dict(), - } + self._as_dict = MappingProxyType( + { + "entity_id": self.entity_id, + "state": self.state, + "attributes": dict(self.attributes), + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + "context": MappingProxyType(self.context.as_dict()), + } + ) return self._as_dict @classmethod diff --git a/tests/test_core.py b/tests/test_core.py index c2d99967a4b4e..81e6ef3a8e511 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -377,10 +377,19 @@ def test_state_as_dict(): "last_updated": last_time.isoformat(), "state": "on", } - assert state.as_dict() == expected + as_dict_1 = state.as_dict() + assert as_dict_1 == expected # 2nd time to verify cache assert state.as_dict() == expected - assert state.as_dict() is state.as_dict() + assert state.as_dict() is as_dict_1 + + # Verify it's immutable + with pytest.raises(AttributeError): + as_dict_1.pop("state") + with pytest.raises(TypeError): + as_dict_1["state"] = "yo" + with pytest.raises(TypeError): + as_dict_1["context"]["user_id"] = None async def test_eventbus_add_remove_listener(hass): From 6cb69515720654a8b63e865abd583b6f6fbbfcc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 09:59:44 -0800 Subject: [PATCH 02/11] Fix type --- homeassistant/components/homekit_controller/diagnostics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index fd404636a220a..b665bc2177eca 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -1,7 +1,7 @@ """Diagnostics support for HomeKit Controller.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from aiohomekit.model.characteristics.characteristic_types import CharacteristicsTypes @@ -66,7 +66,7 @@ def _async_get_diagnostics_for_device( state = hass.states.get(entity_entry.entity_id) state_dict = None if state: - state_dict = async_redact_data(state.as_dict(), REDACTED_STATE) + state_dict = cast(dict, async_redact_data(state.as_dict(), REDACTED_STATE)) state_dict.pop("context", None) entities.append( From ec868f3a7df877aea897f4aa2ee5df9c20aeab3a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 10:49:10 -0800 Subject: [PATCH 03/11] Fix react_data typing --- homeassistant/components/diagnostics/util.py | 12 +++++++++++- .../components/homekit_controller/diagnostics.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index 6154dd14bd21d..d8e4cbff127da 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping -from typing import Any, TypeVar, cast +from typing import Any, TypeVar, cast, overload from homeassistant.core import callback @@ -11,6 +11,16 @@ T = TypeVar("T") +@overload +def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: ignore + ... + + +@overload +def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: + ... + + @callback def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: """Redact sensitive data in a dict.""" diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index b665bc2177eca..fd404636a220a 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -1,7 +1,7 @@ """Diagnostics support for HomeKit Controller.""" from __future__ import annotations -from typing import Any, cast +from typing import Any from aiohomekit.model.characteristics.characteristic_types import CharacteristicsTypes @@ -66,7 +66,7 @@ def _async_get_diagnostics_for_device( state = hass.states.get(entity_entry.entity_id) state_dict = None if state: - state_dict = cast(dict, async_redact_data(state.as_dict(), REDACTED_STATE)) + state_dict = async_redact_data(state.as_dict(), REDACTED_STATE) state_dict.pop("context", None) entities.append( From ccf1896a78ff92180bd1d80cbcd72d359257f2be Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 10:51:18 -0800 Subject: [PATCH 04/11] Also redact mappings stored in dicts --- homeassistant/components/diagnostics/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index d8e4cbff127da..84971ba89f1c2 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -35,7 +35,7 @@ def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: for key, value in redacted.items(): if key in to_redact: redacted[key] = REDACTED - elif isinstance(value, dict): + elif isinstance(value, Mapping): redacted[key] = async_redact_data(value, to_redact) elif isinstance(value, list): redacted[key] = [async_redact_data(item, to_redact) for item in value] From 6b2d532a0e4995a43afe39ae4827856fa924b806 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:01:00 -0800 Subject: [PATCH 05/11] Revert tuya --- homeassistant/components/tuya/diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index 67bbad0aceb5f..f0e5ed2852fae 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -157,7 +157,7 @@ def _async_device_as_dict(hass: HomeAssistant, device: TuyaDevice) -> dict[str, state = hass.states.get(entity_entry.entity_id) state_dict = None if state: - state_dict = dict(state.as_dict()) + state_dict = state.as_dict() # Redact the `entity_picture` attribute as it contains a token. if "entity_picture" in state_dict["attributes"]: From 9d05e046eee89cb627138ab76888d7f9795a7cd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:16:35 -0800 Subject: [PATCH 06/11] Add ReadOnlyDict --- homeassistant/core.py | 18 +++++++++--------- homeassistant/util/__init__.py | 5 ++--- homeassistant/util/read_only_dict.py | 23 +++++++++++++++++++++++ tests/test_core.py | 6 +++--- 4 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 homeassistant/util/read_only_dict.py diff --git a/homeassistant/core.py b/homeassistant/core.py index d9bb6daf5b712..0c44ea52f0757 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -24,7 +24,6 @@ import re import threading from time import monotonic -from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, @@ -83,6 +82,7 @@ run_callback_threadsafe, shutdown_run_callback_threadsafe, ) +from .util.read_only_dict import ReadOnlyDict from .util.timeout import TimeoutManager from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem @@ -1049,12 +1049,12 @@ def __init__( self.entity_id = entity_id.lower() self.state = state - self.attributes = MappingProxyType(attributes or {}) + self.attributes = ReadOnlyDict(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) - self._as_dict: MappingProxyType[str, Collection[Any]] | None = None + self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None @property def name(self) -> str: @@ -1063,7 +1063,7 @@ def name(self) -> str: "_", " " ) - def as_dict(self) -> MappingProxyType[str, Collection[Any]]: + def as_dict(self) -> ReadOnlyDict[str, Collection[Any]]: """Return a dict representation of the State. Async friendly. @@ -1077,14 +1077,14 @@ def as_dict(self) -> MappingProxyType[str, Collection[Any]]: last_updated_isoformat = last_changed_isoformat else: last_updated_isoformat = self.last_updated.isoformat() - self._as_dict = MappingProxyType( + self._as_dict = ReadOnlyDict( { "entity_id": self.entity_id, "state": self.state, - "attributes": dict(self.attributes), + "attributes": self.attributes, "last_changed": last_changed_isoformat, "last_updated": last_updated_isoformat, - "context": MappingProxyType(self.context.as_dict()), + "context": ReadOnlyDict(self.context.as_dict()), } ) return self._as_dict @@ -1345,7 +1345,7 @@ def async_set( last_changed = None else: same_state = old_state.state == new_state and not force_update - same_attr = old_state.attributes == MappingProxyType(attributes) + same_attr = old_state.attributes == attributes last_changed = old_state.last_changed if same_state else None if same_state and same_attr: @@ -1406,7 +1406,7 @@ def __init__( """Initialize a service call.""" self.domain = domain.lower() self.service = service.lower() - self.data = MappingProxyType(data or {}) + self.data = ReadOnlyDict(data or {}) self.context = context or Context() def __repr__(self) -> str: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 3c82639251a8b..5c2882ec2e212 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -2,14 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable, KeysView +from collections.abc import Callable, Coroutine, Iterable, KeysView, Mapping from datetime import datetime, timedelta from functools import wraps import random import re import string import threading -from types import MappingProxyType from typing import Any, TypeVar import slugify as unicode_slug @@ -53,7 +52,7 @@ def slugify(text: str | None, *, separator: str = "_") -> str: def repr_helper(inp: Any) -> str: """Help creating a more readable string representation of objects.""" - if isinstance(inp, (dict, MappingProxyType)): + if isinstance(inp, Mapping): return ", ".join( f"{repr_helper(key)}={repr_helper(item)}" for key, item in inp.items() ) diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py new file mode 100644 index 0000000000000..802f35529dc85 --- /dev/null +++ b/homeassistant/util/read_only_dict.py @@ -0,0 +1,23 @@ +"""Dict utilities.""" +from typing import Any, TypeVar + + +def _readonly(*args: Any, **kwargs: Any) -> Any: + """Raise an exception when a read only dict is modified.""" + raise RuntimeError("Cannot modify ReadOnlyDict") + + +Key = TypeVar("Key") +Value = TypeVar("Value") + + +class ReadOnlyDict(dict[Key, Value]): + """Read only version of dict that is compatible with dict types.""" + + __setitem__ = _readonly + __delitem__ = _readonly + pop = _readonly + popitem = _readonly + clear = _readonly + update = _readonly + setdefault = _readonly diff --git a/tests/test_core.py b/tests/test_core.py index 81e6ef3a8e511..b7cb0e50c7fca 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -384,11 +384,11 @@ def test_state_as_dict(): assert state.as_dict() is as_dict_1 # Verify it's immutable - with pytest.raises(AttributeError): + with pytest.raises(RuntimeError): as_dict_1.pop("state") - with pytest.raises(TypeError): + with pytest.raises(RuntimeError): as_dict_1["state"] = "yo" - with pytest.raises(TypeError): + with pytest.raises(RuntimeError): as_dict_1["context"]["user_id"] = None From 45e18fb8285a08cb044e277e02abef6aa7716bbe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:21:52 -0800 Subject: [PATCH 07/11] Add test --- homeassistant/util/read_only_dict.py | 2 +- tests/util/test_read_only_dict.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/util/test_read_only_dict.py diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py index 802f35529dc85..f9cc949afdce3 100644 --- a/homeassistant/util/read_only_dict.py +++ b/homeassistant/util/read_only_dict.py @@ -1,4 +1,4 @@ -"""Dict utilities.""" +"""Read only dictionary.""" from typing import Any, TypeVar diff --git a/tests/util/test_read_only_dict.py b/tests/util/test_read_only_dict.py new file mode 100644 index 0000000000000..7528c843f5094 --- /dev/null +++ b/tests/util/test_read_only_dict.py @@ -0,0 +1,36 @@ +"""Test read only dictionary.""" +import json + +import pytest + +from homeassistant.util.read_only_dict import ReadOnlyDict + + +def test_read_only_dict(): + """Test read only dictionary.""" + data = ReadOnlyDict({"hello": "world"}) + + with pytest.raises(RuntimeError): + data["hello"] = "universe" + + with pytest.raises(RuntimeError): + data["other_key"] = "universe" + + with pytest.raises(RuntimeError): + data.pop("hello") + + with pytest.raises(RuntimeError): + data.popitem() + + with pytest.raises(RuntimeError): + data.clear() + + with pytest.raises(RuntimeError): + data.update({"yo": "yo"}) + + with pytest.raises(RuntimeError): + data.setdefault("yo", "yo") + + assert isinstance(data, dict) + assert dict(data) == {"hello": "world"} + assert json.dumps(data) == json.dumps({"hello": "world"}) From 4e9037b16d30bafeb2d7fd11991d37b3841856bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:24:30 -0800 Subject: [PATCH 08/11] improve core test --- tests/test_core.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index b7cb0e50c7fca..afb5f50704416 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -39,6 +39,7 @@ ServiceNotFound, ) import homeassistant.util.dt as dt_util +from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import async_capture_events, async_mock_service @@ -378,19 +379,14 @@ def test_state_as_dict(): "state": "on", } as_dict_1 = state.as_dict() + assert isinstance(as_dict_1, ReadOnlyDict) + assert isinstance(as_dict_1["attributes"], ReadOnlyDict) + assert isinstance(as_dict_1["context"], ReadOnlyDict) assert as_dict_1 == expected # 2nd time to verify cache assert state.as_dict() == expected assert state.as_dict() is as_dict_1 - # Verify it's immutable - with pytest.raises(RuntimeError): - as_dict_1.pop("state") - with pytest.raises(RuntimeError): - as_dict_1["state"] = "yo" - with pytest.raises(RuntimeError): - as_dict_1["context"]["user_id"] = None - async def test_eventbus_add_remove_listener(hass): """Test remove_listener method.""" From f5a8a3ba0b0feca04aee76270c9ab52ec16f1e18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:34:52 -0800 Subject: [PATCH 09/11] Cleanup types --- homeassistant/components/esphome/__init__.py | 2 +- homeassistant/components/fan/reproduce_state.py | 7 ++----- homeassistant/components/input_select/reproduce_state.py | 7 ++----- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/light/reproduce_state.py | 7 ++----- homeassistant/components/renault/services.py | 4 ++-- 6 files changed, 10 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ca6eca9ea9f14..7d5736a2e689d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -457,7 +457,7 @@ async def _register_service( } async def execute_service(call: ServiceCall) -> None: - await entry_data.client.execute_service(service, call.data) # type: ignore[arg-type] + await entry_data.client.execute_service(service, call.data) hass.services.async_register( DOMAIN, service_name, execute_service, vol.Schema(schema) diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index c18e8352b24fc..140fdfe917885 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any from homeassistant.const import ( @@ -112,8 +111,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 5a8bd4651c56d..8ba16391d7e49 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION @@ -80,8 +79,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index cdaf5c73e74ae..02e54c9dd732e 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -546,7 +546,7 @@ async def service_exposure_register_modify(self, call: ServiceCall) -> None: replaced_exposure.device.name, ) replaced_exposure.shutdown() - exposure = create_knx_exposure(self.hass, self.xknx, call.data) # type: ignore[arg-type] + exposure = create_knx_exposure(self.hass, self.xknx, call.data) self.service_exposures[group_address] = exposure _LOGGER.debug( "Service exposure_register registered exposure for '%s' - %s", diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 9c382fcb7fac2..d60e0a10f3a63 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any, NamedTuple, cast from homeassistant.const import ( @@ -213,8 +212,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index de69daefef6b5..91dc31d17f7e7 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -1,9 +1,9 @@ """Support for Renault services.""" from __future__ import annotations +from collections.abc import Mapping from datetime import datetime import logging -from types import MappingProxyType from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -126,7 +126,7 @@ async def charge_start(service_call: ServiceCall) -> None: result = await proxy.vehicle.set_charge_start() LOGGER.debug("Charge start result: %s", result) - def get_vehicle_proxy(service_call_data: MappingProxyType) -> RenaultVehicleProxy: + def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy: """Get vehicle from service_call data.""" device_registry = dr.async_get(hass) device_id = service_call_data[ATTR_VEHICLE] From 6d3d6006197324a4fef21d6e656f63297e504e83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:48:06 -0800 Subject: [PATCH 10/11] Shelly types --- homeassistant/components/shelly/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 9a4eb342f71d5..2c81ecbe183ed 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging -from types import MappingProxyType from typing import Any, Final, cast from aioshelly.block_device import Block @@ -140,7 +140,7 @@ def __init__( self.control_result: dict[str, Any] | None = None self.device_block: Block | None = device_block self.last_state: State | None = None - self.last_state_attributes: MappingProxyType[str, Any] + self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] if self.block is not None and self.device_block is not None: From 89fcca9128eac3616c605b056e468a1f0388b8fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 12:40:20 -0800 Subject: [PATCH 11/11] Fix restore cache mocking --- tests/common.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/common.py b/tests/common.py index 3ea4cde2cecb0..3da0fcb98ddf4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -931,9 +931,12 @@ def mock_restore_cache(hass, states): last_states = {} for state in states: restored_state = state.as_dict() - restored_state["attributes"] = json.loads( - json.dumps(restored_state["attributes"], cls=JSONEncoder) - ) + restored_state = { + **restored_state, + "attributes": json.loads( + json.dumps(restored_state["attributes"], cls=JSONEncoder) + ), + } last_states[state.entity_id] = restore_state.StoredState( State.from_dict(restored_state), now )