Skip to content

Commit 2a098b2

Browse files
committed
Add device_tester.py: Interactive tool for MQTT device testing.
1 parent be2f5fe commit 2a098b2

File tree

6 files changed

+344
-2
lines changed

6 files changed

+344
-2
lines changed

inelsmqtt/devices/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ def device_class(self) -> str:
104104
"""
105105
return self.__device_class.TYPE_ID
106106

107+
def get_settable_attributes(self) -> dict:
108+
"""
109+
Returns the settable attributes dictionary.
110+
This method is used ONLY by the IntegrationDeviceTester for testing purposes.
111+
"""
112+
return getattr(self.__device_class, "SETTABLE_ATTRIBUTES", {})
113+
107114
@property
108115
def inels_type(self) -> str:
109116
"""Get inels type of the device

inelsmqtt/protocols/elanrf.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from enum import IntEnum
44
from typing import TYPE_CHECKING, Any, List
55

6+
from inelsmqtt.utils.common import SettableAttribute
7+
68
if TYPE_CHECKING:
79
from inelsmqtt.utils.core import DeviceValue
810

@@ -93,6 +95,9 @@ class Command(IntEnum):
9395

9496
DATA: DataDict = {RELAY: 1}
9597

98+
# NOTE: This dictionary is used ONLY for testing purposes.
99+
SETTABLE_ATTRIBUTES = {"is_on": SettableAttribute("simple_relay.0.is_on", bool, [True, False])}
100+
96101
@classmethod
97102
def COMM_TEST(cls) -> str:
98103
return cls.create_command_payload(cls.Command.COMM_TEST)
@@ -138,6 +143,8 @@ class Command(IntEnum):
138143
TIME_HIGH_BYTE = 0
139144
TIME_LOW_BYTE = 0
140145

146+
SETTABLE_ATTRIBUTES = {"is_on": SettableAttribute("simple_relay.0.is_on", bool, [True, False])}
147+
141148
@classmethod
142149
def COMM_TEST(cls) -> str:
143150
return cls.create_command_payload(cls.Command.COMM_TEST, cls.TIME_HIGH_BYTE, cls.TIME_LOW_BYTE)
@@ -193,6 +200,14 @@ class Command(IntEnum):
193200
Shutter_state.Stop_down: Command.STOP_DOWN,
194201
}
195202

203+
SETTABLE_ATTRIBUTES = {
204+
"state": SettableAttribute(
205+
"shutters.0.state",
206+
Shutter_state,
207+
[Shutter_state.Open, Shutter_state.Closed, Shutter_state.Stop_up, Shutter_state.Stop_down],
208+
),
209+
}
210+
196211
@classmethod
197212
def COMM_TEST(cls) -> str:
198213
return cls.create_command_payload(cls.Command.COMM_TEST, cls.TIME_HIGH_BYTE, cls.TIME_LOW_BYTE)
@@ -235,6 +250,10 @@ class Command(IntEnum):
235250

236251
DATA: DataDict = {RF_DIMMER: [0, 1]}
237252

253+
SETTABLE_ATTRIBUTES = {
254+
"brightness": SettableAttribute("simple_light.0.brightness", int, list(range(0, 101, 10))),
255+
}
256+
238257
@classmethod
239258
def COMM_TEST(cls) -> str:
240259
return cls.create_command_payload(cls.Command.COMM_TEST)
@@ -279,6 +298,10 @@ class Command(IntEnum):
279298

280299
DATA: DataDict = {RF_DIMMER: [0, 1]}
281300

301+
SETTABLE_ATTRIBUTES = {
302+
"brightness": SettableAttribute("simple_light.0.brightness", int, list(range(0, 101, 10))),
303+
}
304+
282305
@classmethod
283306
def COMM_TEST(cls) -> str:
284307
return cls.create_command_payload(cls.Command.COMM_TEST)
@@ -321,6 +344,13 @@ class Command(IntEnum):
321344

322345
DATA: DataDict = {RED: [1], GREEN: [2], BLUE: [3], OUT: [4]}
323346

347+
SETTABLE_ATTRIBUTES = {
348+
"red": SettableAttribute("rgb.0.r", int, list(range(0, 256))),
349+
"green": SettableAttribute("rgb.0.g", int, list(range(0, 256))),
350+
"blue": SettableAttribute("rgb.0.b", int, list(range(0, 256))),
351+
"brightness": SettableAttribute("rgb.0.brightness", int, list(range(0, 256))),
352+
}
353+
324354
@classmethod
325355
def COMM_TEST(cls) -> str:
326356
return cls.create_command_payload(cls.Command.COMM_TEST, 0, 0, 0, 0)
@@ -377,6 +407,10 @@ class Command(IntEnum):
377407
False: Command.OFF,
378408
}
379409

410+
SETTABLE_ATTRIBUTES = {
411+
"is_on": SettableAttribute("simple_relay.0.is_on", bool, [True, False]),
412+
}
413+
380414
@classmethod
381415
def COMM_TEST(cls) -> str:
382416
return cls.create_command_payload(cls.Command.COMM_TEST)
@@ -418,6 +452,10 @@ class DT_09(CommTest):
418452
REQUIRED_TEMP: [3],
419453
}
420454

455+
SETTABLE_ATTRIBUTES = {
456+
"required_temp": SettableAttribute("thermovalve.required", float, [x / 2 for x in range(0, 129)]),
457+
}
458+
421459
@staticmethod
422460
def create_command_payload(temp_required: int) -> str:
423461
return Formatter.format_data([0, temp_required, 0])
@@ -502,6 +540,11 @@ class Command(IntEnum):
502540

503541
DATA: DataDict = {OUT: [4], WHITE: [5]}
504542

543+
SETTABLE_ATTRIBUTES = {
544+
"brightness": SettableAttribute("warm_light.0.brightness", int, list(range(0, 101))),
545+
"relative_ct": SettableAttribute("warm_light.0.relative_ct", int, list(range(0, 101))),
546+
}
547+
505548
@classmethod
506549
def COMM_TEST(cls) -> str:
507550
return cls.create_command_payload(cls.Command.COMM_TEST)
@@ -710,6 +753,15 @@ class Command(IntEnum):
710753
Shutter_state.Stop_down: Command.STOP_DOWN,
711754
}
712755

756+
SETTABLE_ATTRIBUTES = {
757+
"state": SettableAttribute(
758+
"shutters_with_pos.0.state",
759+
Shutter_state,
760+
[Shutter_state.Open, Shutter_state.Closed, Shutter_state.Stop_up, Shutter_state.Stop_down],
761+
),
762+
"position": SettableAttribute("shutters_with_pos.0.position", int, list(range(0, 101))),
763+
}
764+
713765
@classmethod
714766
def COMM_TEST(cls) -> str:
715767
return cls.create_command_payload(cls.Command.COMM_TEST, cls.HIGH_BYTE, cls.LOW_BYTE)

inelsmqtt/utils/common.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,25 @@ class Formatter:
141141
def format_data(data: List[int]) -> str:
142142
"""Formats a list of integers into a newline-separated hexadecimal string with a trailing newline."""
143143
return "\n".join(f"{x:02X}" for x in data) + "\n"
144+
145+
146+
class SettableAttribute:
147+
def __init__(self, route: str, attr_type: type, possible_values: Union[List[Any], Tuple[int, int], None] = None):
148+
self.route = route
149+
self.attr_type = attr_type
150+
self.possible_values = possible_values
151+
152+
def is_valid_value(self, value: Any) -> bool:
153+
if self.possible_values is None:
154+
return True
155+
if isinstance(self.possible_values, tuple) and len(self.possible_values) == 2:
156+
min_val, max_val = self.possible_values
157+
return bool(min_val <= value <= max_val) # Explicit cast to bool
158+
return value in self.possible_values
159+
160+
def get_value_description(self) -> str:
161+
if self.possible_values is None:
162+
return f"Any {self.attr_type.__name__}"
163+
if isinstance(self.possible_values, tuple) and len(self.possible_values) == 2:
164+
return f"Range: {self.possible_values[0]} to {self.possible_values[1]}"
165+
return f"Possible values: {self.possible_values}"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ skip-magic-trailing-comma = false
7979
# Automatically detect and use the appropriate line ending style.
8080
line-ending = "auto"
8181

82-
[tool.ruff.mccabe]
82+
[lint.mccabe]
8383
max-complexity = 10
8484

8585
# https://github.com/home-assistant/core/blob/dev/mypy.ini

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name="elkoep-mqtt",
7-
version="0.2.33.beta.10",
7+
version="0.2.33.beta.11",
88
url="https://github.com/epdevlab/elkoep-mqtt",
99
license="MIT",
1010
author="Elko EP s.r.o.",

0 commit comments

Comments
 (0)