From 431cacaed6595494dab9341b987714718e2d1bb0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 23 Nov 2025 17:17:44 +0100 Subject: [PATCH 1/8] Add kvs_set and kvs_get methods --- aioshelly/rpc_device/device.py | 10 ++++++++++ tests/rpc_device/test_device.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 538db952..596fc1f6 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -581,6 +581,16 @@ async def wall_display_set_screen(self, value: bool) -> None: params = {"on": value} await self.call_rpc("Ui.Screen.Set", params) + async def kvs_get(self, key: str) -> dict[str, Any]: + """Get value from KVS.""" + params = {"key": key} + await self.call_rpc("KVS.Get", params) + + async def kvs_set(self, key: str, value: str) -> None: + """Set value in KVS.""" + params = {"key": key, "value": value} + await self.call_rpc("KVS.Set", params) + async def poll(self) -> None: """Poll device for calls that do not receive push updates.""" calls: list[tuple[str, dict[str, Any] | None]] = [("Shelly.GetStatus", None)] diff --git a/tests/rpc_device/test_device.py b/tests/rpc_device/test_device.py index 016bab39..3231805b 100644 --- a/tests/rpc_device/test_device.py +++ b/tests/rpc_device/test_device.py @@ -1677,3 +1677,35 @@ async def test_wall_display_set_screen( assert call_args_list[0][0][0][0][0] == "Ui.Screen.Set" assert call_args_list[0][0][0][0][1] == {"on": True} + + +@pytest.mark.asyncio +async def test_kvs_set( + rpc_device: RpcDevice, +) -> None: + """Test RpcDevice kvs_set() method.""" + await rpc_device.kvs_set("key1", "value1") + + assert rpc_device.call_rpc_multiple.call_count == 1 + call_args_list = rpc_device.call_rpc_multiple.call_args_list + + assert call_args_list[0][0][0][0][0] == "KVS.Set" + assert call_args_list[0][0][0][0][1] == {"key": "key1", "value": "value1"} + + +@pytest.mark.asyncio +async def test_kvs_get( + rpc_device: RpcDevice, +) -> None: + """Test RpcDevice kvs_get() method.""" + rpc_device.call_rpc_multiple.return_value = [{"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"}] + + result = await rpc_device.kvs_get("key1") + + assert result == {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"} + + assert rpc_device.call_rpc_multiple.call_count == 1 + call_args_list = rpc_device.call_rpc_multiple.call_args_list + + assert call_args_list[0][0][0][0][0] == "KVS.Get" + assert call_args_list[0][0][0][0][1] == {"key": "key1"} From f3acb48c964dac0c6c37e6231f93b80dd3b1afbe Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 25 Nov 2025 15:09:54 +0100 Subject: [PATCH 2/8] Fix kvs_get --- aioshelly/rpc_device/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 596fc1f6..98ac5581 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -584,7 +584,7 @@ async def wall_display_set_screen(self, value: bool) -> None: async def kvs_get(self, key: str) -> dict[str, Any]: """Get value from KVS.""" params = {"key": key} - await self.call_rpc("KVS.Get", params) + return await self.call_rpc("KVS.Get", params) async def kvs_set(self, key: str, value: str) -> None: """Set value in KVS.""" From 585b6a6c916b7f89c7f3451fd605785c67b962f2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 11:27:45 +0100 Subject: [PATCH 3/8] Any --- aioshelly/rpc_device/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 98ac5581..faaa9bda 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -586,7 +586,7 @@ async def kvs_get(self, key: str) -> dict[str, Any]: params = {"key": key} return await self.call_rpc("KVS.Get", params) - async def kvs_set(self, key: str, value: str) -> None: + async def kvs_set(self, key: str, value: Any) -> None: """Set value in KVS.""" params = {"key": key, "value": value} await self.call_rpc("KVS.Set", params) From 554a98384550bb1d9c54f7e555eeeeb8ba4fc71b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 12:39:42 +0100 Subject: [PATCH 4/8] Types --- aioshelly/rpc_device/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index faaa9bda..4e1a23cb 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -586,7 +586,7 @@ async def kvs_get(self, key: str) -> dict[str, Any]: params = {"key": key} return await self.call_rpc("KVS.Get", params) - async def kvs_set(self, key: str, value: Any) -> None: + async def kvs_set(self, key: str, value: str | int | float | bool | None) -> None: """Set value in KVS.""" params = {"key": key, "value": value} await self.call_rpc("KVS.Set", params) From 37ff337f0b9e0befb89c2f650a75d4476eca4ed6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 15:36:48 +0100 Subject: [PATCH 5/8] Lint --- aioshelly/rpc_device/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 4e1a23cb..5dfa9b18 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -586,7 +586,7 @@ async def kvs_get(self, key: str) -> dict[str, Any]: params = {"key": key} return await self.call_rpc("KVS.Get", params) - async def kvs_set(self, key: str, value: str | int | float | bool | None) -> None: + async def kvs_set(self, key: str, value: str | float | bool | None) -> None: """Set value in KVS.""" params = {"key": key, "value": value} await self.call_rpc("KVS.Set", params) From f38480708a65c2a45efc874e5fe688f18ca87c4d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 15:36:55 +0100 Subject: [PATCH 6/8] Format --- tests/rpc_device/test_device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rpc_device/test_device.py b/tests/rpc_device/test_device.py index 3231805b..616af6d8 100644 --- a/tests/rpc_device/test_device.py +++ b/tests/rpc_device/test_device.py @@ -1698,7 +1698,9 @@ async def test_kvs_get( rpc_device: RpcDevice, ) -> None: """Test RpcDevice kvs_get() method.""" - rpc_device.call_rpc_multiple.return_value = [{"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"}] + rpc_device.call_rpc_multiple.return_value = [ + {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"} + ] result = await rpc_device.kvs_get("key1") From 7109603c3ee68ea8aa7a4cd5845335a5ec010896 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 16:20:49 +0100 Subject: [PATCH 7/8] Use json_dumps in kvs_set --- aioshelly/rpc_device/device.py | 8 ++++++-- tests/rpc_device/test_device.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 5dfa9b18..35424099 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -39,6 +39,7 @@ RpcCallError, ShellyError, ) +from ..json import json_dumps from .blerpc import BleRPC from .models import ( ShellyBLEConfig, @@ -586,9 +587,12 @@ async def kvs_get(self, key: str) -> dict[str, Any]: params = {"key": key} return await self.call_rpc("KVS.Get", params) - async def kvs_set(self, key: str, value: str | float | bool | None) -> None: + async def kvs_set( + self, key: str, value: str | float | bool | dict | list | None + ) -> None: """Set value in KVS.""" - params = {"key": key, "value": value} + val = json_dumps(value) if isinstance(value, (dict, list)) else value + params = {"key": key, "value": val} await self.call_rpc("KVS.Set", params) async def poll(self) -> None: diff --git a/tests/rpc_device/test_device.py b/tests/rpc_device/test_device.py index 616af6d8..ff8bd835 100644 --- a/tests/rpc_device/test_device.py +++ b/tests/rpc_device/test_device.py @@ -1679,18 +1679,31 @@ async def test_wall_display_set_screen( assert call_args_list[0][0][0][0][1] == {"on": True} +@pytest.mark.parametrize( + ("in_value", "out_value"), + [ + ("value1", "value1"), + ({"key1": "val1"}, '{"key1":"val1"}'), + ([{"key1": "val1"}, {"key2": "val2"}], '[{"key1":"val1"},{"key2":"val2"}]'), + (42.5, 42.5), + (True, True), + (None, None), + ], +) @pytest.mark.asyncio async def test_kvs_set( rpc_device: RpcDevice, + in_value: str | float | bool | dict | list | None, + out_value: str, ) -> None: """Test RpcDevice kvs_set() method.""" - await rpc_device.kvs_set("key1", "value1") + await rpc_device.kvs_set("key1", in_value) assert rpc_device.call_rpc_multiple.call_count == 1 call_args_list = rpc_device.call_rpc_multiple.call_args_list assert call_args_list[0][0][0][0][0] == "KVS.Set" - assert call_args_list[0][0][0][0][1] == {"key": "key1", "value": "value1"} + assert call_args_list[0][0][0][0][1] == {"key": "key1", "value": out_value} @pytest.mark.asyncio From 331fd71fc16dab9ec921644496075917b976cee3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Nov 2025 16:43:14 +0100 Subject: [PATCH 8/8] Use json_loads in kvs_get --- aioshelly/rpc_device/device.py | 10 ++++++++-- tests/rpc_device/test_device.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/aioshelly/rpc_device/device.py b/aioshelly/rpc_device/device.py index 35424099..3eecd083 100644 --- a/aioshelly/rpc_device/device.py +++ b/aioshelly/rpc_device/device.py @@ -5,6 +5,7 @@ import asyncio import logging from collections.abc import Callable, Iterable +from contextlib import suppress from enum import Enum, auto from functools import partial from typing import TYPE_CHECKING, Any, cast @@ -39,7 +40,7 @@ RpcCallError, ShellyError, ) -from ..json import json_dumps +from ..json import JSONDecodeError, json_dumps, json_loads from .blerpc import BleRPC from .models import ( ShellyBLEConfig, @@ -585,7 +586,12 @@ async def wall_display_set_screen(self, value: bool) -> None: async def kvs_get(self, key: str) -> dict[str, Any]: """Get value from KVS.""" params = {"key": key} - return await self.call_rpc("KVS.Get", params) + response = await self.call_rpc("KVS.Get", params) + + with suppress(JSONDecodeError): + response["value"] = json_loads(response["value"]) + + return response async def kvs_set( self, key: str, value: str | float | bool | dict | list | None diff --git a/tests/rpc_device/test_device.py b/tests/rpc_device/test_device.py index ff8bd835..c6b32c9e 100644 --- a/tests/rpc_device/test_device.py +++ b/tests/rpc_device/test_device.py @@ -1706,18 +1706,31 @@ async def test_kvs_set( assert call_args_list[0][0][0][0][1] == {"key": "key1", "value": out_value} +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("value1", "value1"), + ('{"key1":"val1"}', {"key1": "val1"}), + ('[{"key1":"val1"},{"key2":"val2"}]', [{"key1": "val1"}, {"key2": "val2"}]), + (42.5, 42.5), + (True, True), + (None, None), + ], +) @pytest.mark.asyncio async def test_kvs_get( rpc_device: RpcDevice, + value: str | float | bool | None, + expected: str | float | bool | dict | list | None, ) -> None: """Test RpcDevice kvs_get() method.""" rpc_device.call_rpc_multiple.return_value = [ - {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"} + {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": value} ] result = await rpc_device.kvs_get("key1") - assert result == {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": "value1"} + assert result == {"etag": "16mLia9TRt8lGhj9Zf5Dp6Hw==", "value": expected} assert rpc_device.call_rpc_multiple.call_count == 1 call_args_list = rpc_device.call_rpc_multiple.call_args_list